/*-------------------------------------------------------------------------
 *
 * nbtutils.c
 *	  Postgres btree 实现的实用程序代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/nbtree/nbtutils.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <time.h>

#include "access/nbtree.h"
#include "access/reloptions.h"
#include "access/relscan.h"
#include "catalog/catalog.h"
#include "commands/progress.h"
#include "lib/qunique.h"
#include "miscadmin.h"
#include "utils/array.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"


typedef struct BTSortArrayContext
{
	FmgrInfo	flinfo;
	Oid			collation;
	bool		reverse;
} BTSortArrayContext;

static Datum fc__bt_find_extreme_element(IndexScanDesc fc_scan, ScanKey fc_skey,
									  StrategyNumber fc_strat,
									  Datum *fc_elems, int fc_nelems);
static int	fc__bt_sort_array_elements(IndexScanDesc fc_scan, ScanKey fc_skey,
									bool fc_reverse,
									Datum *fc_elems, int fc_nelems);
static int	fc__bt_compare_array_elements(const void *fc_a, const void *fc_b, void *fc_arg);
static bool fc__bt_compare_scankey_args(IndexScanDesc fc_scan, ScanKey fc_op,
									 ScanKey fc_leftarg, ScanKey fc_rightarg,
									 bool *fc_result);
static bool fc__bt_fix_scankey_strategy(ScanKey fc_skey, int16 *fc_indoption);
static void fc__bt_mark_scankey_required(ScanKey fc_skey);
static bool fc__bt_check_rowcompare(ScanKey fc_skey,
								 IndexTuple fc_tuple, int fc_tupnatts, TupleDesc fc_tupdesc,
								 ScanDirection fc_dir, bool *fc_continuescan);
static int	fc__bt_keep_natts(Relation fc_rel, IndexTuple fc_lastleft,
						   IndexTuple fc_firstright, BTScanInsert fc_itup_key);


/*
 * _bt_mkscankey
 *		构建一个插入扫描键，该键包含来自 itup 的比较数据
 *		以及适合键数据类型的比较器例程。
 *
 *		当 itup 是非支点元组时，返回的插入扫描键
 *		适用于寻找在叶级插入它的位置。支点
 *		元组可用于重新查找具有匹配高键的叶页面，但
 *		然后调用者需要将扫描键的 pivotsearch 字段设置为 true。
 *		这允许调用者查找具有匹配高键的叶页面，
 *		通常位于非支点匹配可能出现的第一个叶页面的左侧。
 *
 *		结果旨在与 _bt_compare() 和 _bt_truncate() 一起使用。
 *		不需要填充插入扫描键参数的调用者
 *		（例如，他们使用临时比较例程，或仅需要用于 _bt_truncate() 的扫描键）
 *		可以传递 NULL 索引元组。扫描键将被初始化， 
 *		就好像传递了 "all truncated" 支点元组。
 *
 *		注意，我们可能偶尔需要共享锁定元数据页面
 *		以确定索引中的键是否预期为唯一（即如果这是一个 "heapkeyspace" 索引）。
 *		当调用者传递 NULL 元组时，我们假设这是一个 heapkeyspace 索引，
 *		允许索引构建调用者避免访问不存在的元数据页面。
 *		当传递 NULL 元组时，我们还假设该索引不是 allequalimage；
 *		CREATE INDEX 调用者调用 _bt_allequalimage() 自行设置该字段。
 */
BTScanInsert
_bt_mkscankey(Relation fc_rel, IndexTuple fc_itup)
{
	BTScanInsert fc_key;
	ScanKey		fc_skey;
	TupleDesc	fc_itupdesc;
	int			fc_indnkeyatts;
	int16	   *fc_indoption;
	int			fc_tupnatts;
	int			fc_i;

	fc_itupdesc = RelationGetDescr(fc_rel);
	fc_indnkeyatts = IndexRelationGetNumberOfKeyAttributes(fc_rel);
	fc_indoption = fc_rel->rd_indoption;
	fc_tupnatts = fc_itup ? BTreeTupleGetNAtts(fc_itup, fc_rel) : 0;

	Assert(fc_tupnatts <= IndexRelationGetNumberOfAttributes(fc_rel));

	/*
	 * 我们将使用基于键列构建的扫描键执行搜索。
	 * 被截断的属性和非键属性从最终
	 * 扫描键中省略。
	 */
	fc_key = palloc(offsetof(BTScanInsertData, scankeys) +
				 sizeof(ScanKeyData) * fc_indnkeyatts);
	if (fc_itup)
		_bt_metaversion(fc_rel, &fc_key->heapkeyspace, &fc_key->allequalimage);
	else
	{
		/* 实用程序语句调用者可以自行设置这些字段 */
		fc_key->heapkeyspace = true;
		fc_key->allequalimage = false;
	}
	fc_key->anynullkeys = false;	/* 初始假设 */
	fc_key->nextkey = false;
	fc_key->pivotsearch = false;
	fc_key->keysz = Min(fc_indnkeyatts, fc_tupnatts);
	fc_key->scantid = fc_key->heapkeyspace && fc_itup ?
		BTreeTupleGetHeapTID(fc_itup) : NULL;
	fc_skey = fc_key->scankeys;
	for (fc_i = 0; fc_i < fc_indnkeyatts; fc_i++)
	{
		FmgrInfo   *fc_procinfo;
		Datum		fc_arg;
		bool		fc_null;
		int			fc_flags;

		/*
		 * 由于不需要跨类型比较，我们可以使用
		 * 缓存的（默认）支持过程。
		 */
		fc_procinfo = index_getprocinfo(fc_rel, fc_i + 1, BTORDER_PROC);

		/*
		 * 从被截断的属性构建的键参数（或者当调用者
		 * 不提供元组时）被防御性地表示为 NULL 值。它们
		 * 不应该被使用。
		 */
		if (fc_i < fc_tupnatts)
			fc_arg = index_getattr(fc_itup, fc_i + 1, fc_itupdesc, &fc_null);
		else
		{
			fc_arg = (Datum) 0;
			fc_null = true;
		}
		fc_flags = (fc_null ? SK_ISNULL : 0) | (fc_indoption[fc_i] << SK_BT_INDOPTION_SHIFT);
		ScanKeyEntryInitializeWithInfo(&fc_skey[fc_i],
									   fc_flags,
									   (AttrNumber) (fc_i + 1),
									   InvalidStrategy,
									   InvalidOid,
									   fc_rel->rd_indcollation[fc_i],
									   fc_procinfo,
									   fc_arg);
		/* 记录任何键属性是否为 NULL（或被截断） */
		if (fc_null)
			fc_key->anynullkeys = true;
	}

	/*
	 * 在 NULLS NOT DISTINCT 模式下，我们假装没有 null 键，
	 * 以便进行完整的唯一性检查。
	 */
	if (fc_rel->rd_index->indnullsnotdistinct)
		fc_key->anynullkeys = false;

	return fc_key;
}

/*
 * 释放由 _bt_search 创建的回溯栈。
 */
void _bt_freestack(BTStack fc_stack)
{
	BTStack		fc_ostack;

	while (fc_stack != NULL)
	{
		fc_ostack = fc_stack;
		fc_stack = fc_stack->bts_parent;
		pfree(fc_ostack);
	}
}


/*
 *	_bt_preprocess_array_keys() -- 预处理 SK_SEARCHARRAY 扫描键
 *
 * 如果存在任何 SK_SEARCHARRAY 扫描键，要对数组进行解构，并为每个等值类型键设置 BTArrayKeyInfo 信息。
 * 在 so->arrayKeyData 中准备修改后的扫描键，这将在每个原始索引扫描操作期间保存当前数组元素。对于不等式数组键，找到极值元素并用该标量值替换整个数组就足够了。
 *
 * 注意：我们需要 so->arrayKeyData，而不仅仅是在 scan->keyData 上乱写，因为调用者可以在不提供新一组扫描键数据的情况下调用 btrescan。
 */
void _bt_preprocess_array_keys(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	int			fc_numberOfKeys = fc_scan->numberOfKeys;
	int16	   *fc_indoption = fc_scan->indexRelation->rd_indoption;
	int			fc_numArrayKeys;
	ScanKey		fc_cur;
	int			fc_i;
	MemoryContext fc_oldContext;

	/* 快速检查是否存在任何数组键 */
	fc_numArrayKeys = 0;
	for (fc_i = 0; fc_i < fc_numberOfKeys; fc_i++)
	{
		fc_cur = &fc_scan->keyData[fc_i];
		if (fc_cur->sk_flags & SK_SEARCHARRAY)
		{
			fc_numArrayKeys++;
			Assert(!(fc_cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
			/* 如果任何数组整体为 null，我们可以立即退出。 */
			if (fc_cur->sk_flags & SK_ISNULL)
			{
				fc_so->numArrayKeys = -1;
				fc_so->arrayKeyData = NULL;
				return;
			}
		}
	}

	/* 如果没有任务可做，则退出。 */
	if (fc_numArrayKeys == 0)
	{
		fc_so->numArrayKeys = 0;
		fc_so->arrayKeyData = NULL;
		return;
	}

	/*
	 * 创建一个扫描生命周期上下文来保存与数组相关的数据，或在我们已经拥有来自先前重新扫描循环的上下文时重置它。
	 */
	if (fc_so->arrayContext == NULL)
		fc_so->arrayContext = AllocSetContextCreate(CurrentMemoryContext,
												 "BTree array context",
												 ALLOCSET_SMALL_SIZES);
	else
		MemoryContextReset(fc_so->arrayContext);

	fc_oldContext = MemoryContextSwitchTo(fc_so->arrayContext);

	/* 在工作区上下文中创建可修改的 scan->keyData 副本 */
	fc_so->arrayKeyData = (ScanKey) palloc(fc_scan->numberOfKeys * sizeof(ScanKeyData));
	memcpy(fc_so->arrayKeyData,
		   fc_scan->keyData,
		   fc_scan->numberOfKeys * sizeof(ScanKeyData));

	/* 在工作区上下文中为每个数组数据分配空间 */
	fc_so->arrayKeys = (BTArrayKeyInfo *) palloc0(fc_numArrayKeys * sizeof(BTArrayKeyInfo));

	/* 现在处理每个数组键 */
	fc_numArrayKeys = 0;
	for (fc_i = 0; fc_i < fc_numberOfKeys; fc_i++)
	{
		ArrayType  *fc_arrayval;
		int16		fc_elmlen;
		bool		fc_elmbyval;
		char		fc_elmalign;
		int			fc_num_elems;
		Datum	   *fc_elem_values;
		bool	   *fc_elem_nulls;
		int			fc_num_nonnulls;
		int			fc_j;

		fc_cur = &fc_so->arrayKeyData[fc_i];
		if (!(fc_cur->sk_flags & SK_SEARCHARRAY))
			continue;

		/*
		 * 首先，将数组解构为元素。 这里分配的任何内容（包括可能被解除烘焙的数组值）都在工作区上下文中。
		 */
		fc_arrayval = DatumGetArrayTypeP(fc_cur->sk_argument);
		/* 我们可以缓存这些数据，但没有必要清除，值得付出代价 */
		get_typlenbyvalalign(ARR_ELEMTYPE(fc_arrayval),
							 &fc_elmlen, &fc_elmbyval, &fc_elmalign);
		deconstruct_array(fc_arrayval,
						  ARR_ELEMTYPE(fc_arrayval),
						  fc_elmlen, fc_elmbyval, fc_elmalign,
						  &fc_elem_values, &fc_elem_nulls, &fc_num_elems);

		/*
		 * 压缩掉任何 null 元素。 我们可以忽略它们，因为我们假设所有 btree 操作都是严格的。
		 */
		fc_num_nonnulls = 0;
		for (fc_j = 0; fc_j < fc_num_elems; fc_j++)
		{
			if (!fc_elem_nulls[fc_j])
				fc_elem_values[fc_num_nonnulls++] = fc_elem_values[fc_j];
		}

		/* 我们现在可以释放 elem_nulls，但没有必要这几乎没有价值 */

		/* 如果没有非 null 元素，扫描条件是不可满足的 */
		if (fc_num_nonnulls == 0)
		{
			fc_numArrayKeys = -1;
			break;
		}

		/*
		 * 如果比较运算符不是等值运算，则数组条件退化为与最小或最大非 null 数组元素进行简单比较，视情况而定。
		 */
		switch (fc_cur->sk_strategy)
		{
			case BTLessStrategyNumber:
			case BTLessEqualStrategyNumber:
				fc_cur->sk_argument =
					fc__bt_find_extreme_element(fc_scan, fc_cur,
											 BTGreaterStrategyNumber,
											 fc_elem_values, fc_num_nonnulls);
				continue;
			case BTEqualStrategyNumber:
				/* 继续循环的其余部分 */
				break;
			case BTGreaterEqualStrategyNumber:
			case BTGreaterStrategyNumber:
				fc_cur->sk_argument =
					fc__bt_find_extreme_element(fc_scan, fc_cur,
											 BTLessStrategyNumber,
											 fc_elem_values, fc_num_nonnulls);
				continue;
			default:
				elog(ERROR, "unrecognized StrategyNumber: %d",
					 (int) fc_cur->sk_strategy);
				break;
		}

		/*
		 * 对非 null 元素进行排序，并消除任何重复项。 我们必须按照索引列使用的相同顺序进行排序，这样连续的原始索引扫描产生的数据才能按索引顺序组织。
		 */
		fc_num_elems = fc__bt_sort_array_elements(fc_scan, fc_cur,
											(fc_indoption[fc_cur->sk_attno - 1] & INDOPTION_DESC) != 0,
											fc_elem_values, fc_num_nonnulls);

		/*
		 * 并设置 BTArrayKeyInfo 数据。
		 */
		fc_so->arrayKeys[fc_numArrayKeys].scan_key = fc_i;
		fc_so->arrayKeys[fc_numArrayKeys].num_elems = fc_num_elems;
		fc_so->arrayKeys[fc_numArrayKeys].elem_values = fc_elem_values;
		fc_numArrayKeys++;
	}

	fc_so->numArrayKeys = fc_numArrayKeys;

	MemoryContextSwitchTo(fc_oldContext);
}

/*
 * _bt_find_extreme_element() -- 获取最小或最大数组元素
 *
 * scan 和 skey 确定索引列，其 opfamily 决定比较语义。 strat 应该是 BTLessStrategyNumber 来获取最小元素，或 BTGreaterStrategyNumber 来获取最大元素。
 */
static Datum fc__bt_find_extreme_element(IndexScanDesc fc_scan, ScanKey fc_skey,
						 StrategyNumber fc_strat,
						 Datum *fc_elems, int fc_nelems)
{
	Relation	fc_rel = fc_scan->indexRelation;
	Oid			fc_elemtype,
				fc_cmp_op;
	RegProcedure fc_cmp_proc;
	FmgrInfo	fc_flinfo;
	Datum		fc_result;
	int			fc_i;

	/*
	 * 确定数组元素的名义数据类型。 我们必须支持约定，即 sk_subtype == InvalidOid 表示 opclass 输入类型；这是一个黑客，简化 ScanKeyInit() 的使用。
	 */
	fc_elemtype = fc_skey->sk_subtype;
	if (fc_elemtype == InvalidOid)
		fc_elemtype = fc_rel->rd_opcintype[fc_skey->sk_attno - 1];

	/*
	 * 在操作族中查找适当的比较操作符。
	 *
	 * 注意：如果操作族不完整，则可能会失败，但操作族省略
	 * 支持的任何数据类型的非交叉类型比较操作符的可能性似乎非常小。
	 */
	fc_cmp_op = get_opfamily_member(fc_rel->rd_opfamily[fc_skey->sk_attno - 1],
								 fc_elemtype,
								 fc_elemtype,
								 fc_strat);
	if (!OidIsValid(fc_cmp_op))
		elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
			 fc_strat, fc_elemtype, fc_elemtype,
			 fc_rel->rd_opfamily[fc_skey->sk_attno - 1]);
	fc_cmp_proc = get_opcode(fc_cmp_op);
	if (!RegProcedureIsValid(fc_cmp_proc))
		elog(ERROR, "missing oprcode for operator %u", fc_cmp_op);

	fmgr_info(fc_cmp_proc, &fc_flinfo);

	Assert(fc_nelems > 0);
	fc_result = fc_elems[0];
	for (fc_i = 1; fc_i < fc_nelems; fc_i++)
	{
		if (DatumGetBool(FunctionCall2Coll(&fc_flinfo,
										   fc_skey->sk_collation,
										   fc_elems[fc_i],
										   fc_result)))
			fc_result = fc_elems[fc_i];
	}

	return fc_result;
}

/*
 * _bt_sort_array_elements() -- 对数组元素进行排序和去重
 *
 * 数组元素就地排序，并返回去重后的新元素数量。
 *
 * scan 和 skey 确定索引列，其操作族决定了
 * 比较语义。如果 reverse 为真，则按降序排序。
 */
static int fc__bt_sort_array_elements(IndexScanDesc fc_scan, ScanKey fc_skey,
						bool fc_reverse,
						Datum *fc_elems, int fc_nelems)
{
	Relation	fc_rel = fc_scan->indexRelation;
	Oid			fc_elemtype;
	RegProcedure fc_cmp_proc;
	BTSortArrayContext fc_cxt;

	if (fc_nelems <= 1)
		return fc_nelems;			/* 没有工作要做 */

	/*
	 * 确定数组元素的名义数据类型。 我们必须支持约定，即 sk_subtype == InvalidOid 表示 opclass 输入类型；这是一个黑客，简化 ScanKeyInit() 的使用。
	 */
	fc_elemtype = fc_skey->sk_subtype;
	if (fc_elemtype == InvalidOid)
		fc_elemtype = fc_rel->rd_opcintype[fc_skey->sk_attno - 1];

	/*
	 * 在操作族中查找适当的比较函数。
	 *
	 * 注意：如果操作族不完整，则可能会失败，但操作族省略
	 * 对于其所支持的任何数据类型，非交叉类型支持函数的可能性似乎非常小。
	 */
	fc_cmp_proc = get_opfamily_proc(fc_rel->rd_opfamily[fc_skey->sk_attno - 1],
								 fc_elemtype,
								 fc_elemtype,
								 BTORDER_PROC);
	if (!RegProcedureIsValid(fc_cmp_proc))
		elog(ERROR, "missing support function %d(%u,%u) in opfamily %u",
			 BTORDER_PROC, fc_elemtype, fc_elemtype,
			 fc_rel->rd_opfamily[fc_skey->sk_attno - 1]);

	/* 对数组元素进行排序 */
	fmgr_info(fc_cmp_proc, &fc_cxt.flinfo);
	fc_cxt.collation = fc_skey->sk_collation;
	fc_cxt.reverse = fc_reverse;
	qsort_arg((void *) fc_elems, fc_nelems, sizeof(Datum),
			  fc__bt_compare_array_elements, (void *) &fc_cxt);

	/* 现在扫描已排序的元素并移除重复项 */
	return qunique_arg(fc_elems, fc_nelems, sizeof(Datum),
					   fc__bt_compare_array_elements, &fc_cxt);
}

/*
 * qsort_arg 比较器用于排序数组元素
 */
static int fc__bt_compare_array_elements(const void *fc_a, const void *fc_b, void *fc_arg)
{
	Datum		fc_da = *((const Datum *) fc_a);
	Datum		fc_db = *((const Datum *) fc_b);
	BTSortArrayContext *fc_cxt = (BTSortArrayContext *) fc_arg;
	int32		fc_compare;

	fc_compare = DatumGetInt32(FunctionCall2Coll(&fc_cxt->flinfo,
											  fc_cxt->collation,
											  fc_da, fc_db));
	if (fc_cxt->reverse)
		INVERT_COMPARE_RESULT(fc_compare);
	return fc_compare;
}

/*
 * _bt_start_array_keys() -- 在扫描开始时初始化数组键
 *
 * 设置 cur_elem 计数器，并填充每个数组 scankey 的第一个 sk_argument 值。我们不能在不知道扫描方向的情况下执行此操作。
 */
void _bt_start_array_keys(IndexScanDesc fc_scan, ScanDirection fc_dir)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	int			fc_i;

	for (fc_i = 0; fc_i < fc_so->numArrayKeys; fc_i++)
	{
		BTArrayKeyInfo *fc_curArrayKey = &fc_so->arrayKeys[fc_i];
		ScanKey		fc_skey = &fc_so->arrayKeyData[fc_curArrayKey->scan_key];

		Assert(fc_curArrayKey->num_elems > 0);
		if (ScanDirectionIsBackward(fc_dir))
			fc_curArrayKey->cur_elem = fc_curArrayKey->num_elems - 1;
		else
			fc_curArrayKey->cur_elem = 0;
		fc_skey->sk_argument = fc_curArrayKey->elem_values[fc_curArrayKey->cur_elem];
	}

	fc_so->arraysStarted = true;
}

/*
 * _bt_advance_array_keys() -- 前进到下一组数组元素
 *
 * 如果还有另一组值需要考虑则返回真，否则返回假。
 * 如果返回真，则 scankeys 会使用下一组值初始化。
 */
bool _bt_advance_array_keys(IndexScanDesc fc_scan, ScanDirection fc_dir)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	bool		fc_found = false;
	int			fc_i;

	/*
	 * 我们必须尽快前进到最后的数组键，因为它将
	 * 对应于可用资格中最低阶的索引列。这是必要的，以确保在有多个
	 * 数组键时输出的正确顺序。
	 */
	for (fc_i = fc_so->numArrayKeys - 1; fc_i >= 0; fc_i--)
	{
		BTArrayKeyInfo *fc_curArrayKey = &fc_so->arrayKeys[fc_i];
		ScanKey		fc_skey = &fc_so->arrayKeyData[fc_curArrayKey->scan_key];
		int			fc_cur_elem = fc_curArrayKey->cur_elem;
		int			fc_num_elems = fc_curArrayKey->num_elems;

		if (ScanDirectionIsBackward(fc_dir))
		{
			if (--fc_cur_elem < 0)
			{
				fc_cur_elem = fc_num_elems - 1;
				fc_found = false;	/* 需要前进到下一个数组键 */
			}
			else
				fc_found = true;
		}
		else
		{
			if (++fc_cur_elem >= fc_num_elems)
			{
				fc_cur_elem = 0;
				fc_found = false;	/* 需要前进到下一个数组键 */
			}
			else
				fc_found = true;
		}

		fc_curArrayKey->cur_elem = fc_cur_elem;
		fc_skey->sk_argument = fc_curArrayKey->elem_values[fc_cur_elem];
		if (fc_found)
			break;
	}

	/* 前进并行扫描 */
	if (fc_scan->parallel_scan != NULL)
		_bt_parallel_advance_array_keys(fc_scan);

	/*
	 * 当未找到新的数组键时，扫描会“超过数组键的末尾”。
	 * 如果需要重新扫描，_bt_start_array_keys 仍然可以“重启”数组键。
	 */
	if (!fc_found)
		fc_so->arraysStarted = false;

	return fc_found;
}

/*
 * _bt_mark_array_keys() -- 在 btmarkpos 期间处理数组键
 *
 * 将数组键的当前状态保存为“标记”位置。
 */
void _bt_mark_array_keys(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	int			fc_i;

	for (fc_i = 0; fc_i < fc_so->numArrayKeys; fc_i++)
	{
		BTArrayKeyInfo *fc_curArrayKey = &fc_so->arrayKeys[fc_i];

		fc_curArrayKey->mark_elem = fc_curArrayKey->cur_elem;
	}
}

/*
 * _bt_restore_array_keys() -- 在 btrestrpos 期间处理数组键
 *
 * 将数组键恢复到设置标记时的位置。
 */
void _bt_restore_array_keys(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	bool		fc_changed = false;
	int			fc_i;

	/* 恢复每个数组键到设置标记时的位置 */
	for (fc_i = 0; fc_i < fc_so->numArrayKeys; fc_i++)
	{
		BTArrayKeyInfo *fc_curArrayKey = &fc_so->arrayKeys[fc_i];
		ScanKey		fc_skey = &fc_so->arrayKeyData[fc_curArrayKey->scan_key];
		int			fc_mark_elem = fc_curArrayKey->mark_elem;

		if (fc_curArrayKey->cur_elem != fc_mark_elem)
		{
			fc_curArrayKey->cur_elem = fc_mark_elem;
			fc_skey->sk_argument = fc_curArrayKey->elem_values[fc_mark_elem];
			fc_changed = true;
		}
	}

	/*
	 * 如果我们更改了任何键，则必须重新执行 _bt_preprocess_keys。这听起来
	 * 可能有些过分，但在每个索引列有多个键的情况下，似乎有必要做这一整套准备工作。
	 *
	 * 每当扫描的数组键在最后一个原始索引扫描结束时“回绕”时，也需要这样做。
	 * 从其他地方在回绕之后不会调用 _bt_preprocess_keys，因此我们自己来做。
	 */
	if (fc_changed || !fc_so->arraysStarted)
	{
		_bt_preprocess_keys(fc_scan);
		/* 标记应该已在一致的键集上设置... */
		Assert(fc_so->qual_ok);
	}
}


/*
 *	_bt_preprocess_keys() — 预处理扫描键
 *
 * 给定的搜索类型键 (在 scan->keyData[] 或 so->arrayKeyData[] 中)
 * 被复制到 so->keyData[]，并可能进行转换。
 * scan->numberOfKeys 是输入键的数量，so->numberOfKeys 获取
 * 输出键的数量（可能更少，绝不会更多）。
 *
 * 输出键带有额外的 sk_flags 位，超出调用者提供的
 * 系统标准位。相关索引属性的 DESC 和 NULLS_FIRST
 * 选项位被复制到标志中。此外，对于 DESC 列，我们交换（翻转）所有的 sk_strategy 数字
 * 以便索引按照所需方向排序。
 *
 * 此例程的一个主要目的在于发现哪些扫描键必须被满足以继续扫描。
 * 它还试图消除冗余键并检测矛盾键。（如果索引操作族提供
 * 不完整的跨类型操作符集合，我们可能会未能检测到冗余
 * 或矛盾键，但我们可以承受这一点。）
 *
 * 输出键必须按索引属性排序。目前我们期待（但会进行验证）
 * 输入键已经按此排序 —— 这一点在 indxpath.c 的 match_clauses_to_index() 中完成。
 * 在此处处理的过程中，每个属性内的键可能会有一些重新排序，
 * 但没有其他代码依赖于此。
 *
 * 如果它们必须被满足以继续向前或向后扫描，输出键将被标记为 SK_BT_REQFWD 和/或 SK_BT_REQBKWD。
 * _bt_checkkeys 使用这些标志。例如，如果条件是 "x = 1 AND y < 4 AND z < 5"，那么 _bt_checkkeys 将拒绝元组
 * (1,2,7)，但我们必须继续扫描，以防还有元组 (1,3,z)。
 * 一旦我们达到元组 (1,4,z) 就可以停止扫描，因为没有
 * 后续元组可以匹配。这通过标记 x 和 y 键，
 * 但不标记 z 键为 SK_BT_REQFWD 体现出来。一般来说，
 * 具有 "=" 键的前导属性的键被标记为 SK_BT_REQFWD 和 SK_BT_REQBKWD。
 * 对于第一个没有 "=" 键的属性，任何 "<" 和 "<=" 键被标记为 SK_BT_REQFWD，而任何 ">" 和 ">=" 键被标记为 SK_BT_REQBKWD。
 * 通过考虑上述示例可以看到这一点。特别注意，如果某一属性没有键，
 * 之后属性的键永远不可能被要求；例如 "WHERE y = 4"
 * 需要一个完整的索引扫描。
 *
 * 如果可能，将消除冗余键：我们仅保留最严密的
 * >/>= 界限和最严密的 </<= 界限，如果有 "=" 键，那么
 * 这将是返回的唯一一个。（因此，我们返回单个 = 键，
 * 或每个属性的一两个边界条件键。）然而，如果我们无法
 * 因缺少合适的跨类型操作符而比较两个键，
 * 我们也无法消除其中任何一个。如果存在两个具有相同
 * 操作策略的键，第二个键将被直接推入输出数组
 * ，而不在此处进一步处理。如果我们无法比较它们，
 * 也可能会发出两个 >/>= 或两个 </<= 键。即使我们不消除
 * 冗余键，关于需要键的逻辑仍然适用。
 *
 * 请注意，我们需要方向敏感的必需键标志的一个原因
 * 正是我们可能无法消除冗余键。假设我们有 "x > 4::int AND x > 10::bigint"，并且由于缺少合适的跨类型操作符
 * 而无法确定哪个键更严格。_bt_first 将任意选择其中一个键进行初始
 * 定位。如果选择了 x > 4，则 x > 10 的条件将失败
 * ，直到我们达到索引条目 > 10；但我们不能仅仅因为
 * x > 10 失败而停止扫描。另一方面，如果我们向后扫描，任何一个键的失败
 * 确实足以停止扫描。（一般来说，当存在不等式键时，
 * 初始定位代码只承诺在第一个可能匹配之前进行定位，而不在
 * 第一个匹配处进行定位，正向扫描如此；或在最后一个匹配之后进行反向扫描。）
 *
 * 作为此工作的副产品，我们可以检测到矛盾的条件
 * ，例如 "x = 1 AND x > 2"。如果我们看到这一点，我们返回 so->qual_ok = false，
 * 指示扫描根本不需要运行，因为没有元组可以匹配。
 * （在这种情况下，我们不打算完成输出键数组！）
 * 同样，缺失的跨类型操作符可能导致我们未能证明
 * 条件矛盾，但扫描将正确工作。
 *
 * 行比较键目前也被处理得没有任何智商：
 * 我们只是将它们传输到预处理数组中，而不进行任何
 * 编辑。就必需键的逻辑而言，我们可以将它们视为
 * 对行的第一个索引列进行普通的不等号比较。
 *
 * 注意：我们必须将预处理的扫描键复制到私有
 * 存储的原因是我们正在根据键参数值的比较来修改数组，
 * 这些值可能在重新扫描或移动到新的数组键元素后发生变化。
 * 因此，我们无法覆盖源数据。
 */
void _bt_preprocess_keys(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	int			fc_numberOfKeys = fc_scan->numberOfKeys;
	int16	   *fc_indoption = fc_scan->indexRelation->rd_indoption;
	int			fc_new_numberOfKeys;
	int			fc_numberOfEqualCols;
	ScanKey		fc_inkeys;
	ScanKey		fc_outkeys;
	ScanKey		fc_cur;
	ScanKey		fc_xform[BTMaxStrategyNumber];
	bool		fc_test_result;
	int			fc_i,
				fc_j;
	AttrNumber	fc_attno;

	/* 初始化结果变量 */
	fc_so->qual_ok = true;
	fc_so->numberOfKeys = 0;

	if (fc_numberOfKeys < 1)
		return;					/* 如果没有条件扫描则完成 */

	/*
	 * 如果存在数组键，则读取 so->arrayKeyData， 否则读取 scan->keyData
	 */
	if (fc_so->arrayKeyData != NULL)
		fc_inkeys = fc_so->arrayKeyData;
	else
		fc_inkeys = fc_scan->keyData;

	fc_outkeys = fc_so->keyData;
	fc_cur = &fc_inkeys[0];
	/* 我们检查输入键是否正确排序 */
	if (fc_cur->sk_attno < 1)
		elog(ERROR, "btree index keys must be ordered by attribute");

	/* 如果只有一个键，我们可以短路大部分工作 */
	if (fc_numberOfKeys == 1)
	{
		/* 对 scankey 应用 indoption（可能会改变 sk_strategy！） */
		if (!fc__bt_fix_scankey_strategy(fc_cur, fc_indoption))
			fc_so->qual_ok = false;
		memcpy(fc_outkeys, fc_cur, sizeof(ScanKeyData));
		fc_so->numberOfKeys = 1;
		/* 如果是第一个索引列，则可以将条件标记为必需 */
		if (fc_cur->sk_attno == 1)
			fc__bt_mark_scankey_required(fc_outkeys);
		return;
	}

	/*
	 * 否则，执行完整的处理。
	 */
	fc_new_numberOfKeys = 0;
	fc_numberOfEqualCols = 0;

	/*
	 * 为属性 1 的键处理初始化。
	 *
	 * xform[i] 指向当前最佳的策略类型为 i+1 的扫描键；如果我们尚未
	 * 找到此属性的扫描键，则为 NULL。
	 */
	fc_attno = 1;
	memset(fc_xform, 0, sizeof(fc_xform));

	/*
	 * 循环从 0 到 numberOfKeys（包括）；我们使用最后一次传递来
	 * 处理最后键后的处理。实际退出循环在下面的 “break” 语句中。
	 */
	for (fc_i = 0;; fc_cur++, fc_i++)
	{
		if (fc_i < fc_numberOfKeys)
		{
			/* 对 scankey 应用 indoption（可能会改变 sk_strategy！） */
			if (!fc__bt_fix_scankey_strategy(fc_cur, fc_indoption))
			{
				/* NULL 不能匹配，因此放弃 */
				fc_so->qual_ok = false;
				return;
			}
		}

		/*
		 * 如果我们在特定属性的键末尾，完成处理并发出清理后的键。
		 */
		if (fc_i == fc_numberOfKeys || fc_cur->sk_attno != fc_attno)
		{
			int			fc_priorNumberOfEqualCols = fc_numberOfEqualCols;

			/* 检查输入键是否正确排序 */
			if (fc_i < fc_numberOfKeys && fc_cur->sk_attno < fc_attno)
				elog(ERROR, "btree index keys must be ordered by attribute");

			/*
			 * 如果已指定 =，则所有其他键可以被消除为
			 * 多余的。如果我们有一个像 key = 1 AND key > 2 的情况，
			 * 我们可以将 qual_ok 设置为 false 并放弃进一步处理。
			 *
			 * 我们还必须处理“key IS NULL”的情况，在与任何其他索引条件
			 * 结合时是不可满足的。当我们到达这里时，它已被分类为等式
			 * 检查，并且我们已经拒绝了它与常规等式条件的任何组合；但与其他
			 * 类型的条件除外。
			 */
			if (fc_xform[BTEqualStrategyNumber - 1])
			{
				ScanKey		fc_eq = fc_xform[BTEqualStrategyNumber - 1];

				for (fc_j = BTMaxStrategyNumber; --fc_j >= 0;)
				{
					ScanKey		fc_chk = fc_xform[fc_j];

					if (!fc_chk || fc_j == (BTEqualStrategyNumber - 1))
						continue;

					if (fc_eq->sk_flags & SK_SEARCHNULL)
					{
						/* IS NULL 与其他任何条件矛盾 */
						fc_so->qual_ok = false;
						return;
					}

					if (fc__bt_compare_scankey_args(fc_scan, fc_chk, fc_eq, fc_chk,
												 &fc_test_result))
					{
						if (!fc_test_result)
						{
							/* 键被证明是相互矛盾的 */
							fc_so->qual_ok = false;
							return;
						}
						/* 否则丢弃冗余的不等键 */
						fc_xform[fc_j] = NULL;
					}
					/* 否则，无法确定冗余性，保留两个键 */
				}
				/* 跟踪我们有“=”键的属性的数量 */
				fc_numberOfEqualCols++;
			}

			/* 尝试只保留一个 <, <= */
			if (fc_xform[BTLessStrategyNumber - 1]
				&& fc_xform[BTLessEqualStrategyNumber - 1])
			{
				ScanKey		fc_lt = fc_xform[BTLessStrategyNumber - 1];
				ScanKey		fc_le = fc_xform[BTLessEqualStrategyNumber - 1];

				if (fc__bt_compare_scankey_args(fc_scan, fc_le, fc_lt, fc_le,
											 &fc_test_result))
				{
					if (fc_test_result)
						fc_xform[BTLessEqualStrategyNumber - 1] = NULL;
					else
						fc_xform[BTLessStrategyNumber - 1] = NULL;
				}
			}

			/* 尝试只保留一个 >, >= */
			if (fc_xform[BTGreaterStrategyNumber - 1]
				&& fc_xform[BTGreaterEqualStrategyNumber - 1])
			{
				ScanKey		fc_gt = fc_xform[BTGreaterStrategyNumber - 1];
				ScanKey		fc_ge = fc_xform[BTGreaterEqualStrategyNumber - 1];

				if (fc__bt_compare_scankey_args(fc_scan, fc_ge, fc_gt, fc_ge,
											 &fc_test_result))
				{
					if (fc_test_result)
						fc_xform[BTGreaterEqualStrategyNumber - 1] = NULL;
					else
						fc_xform[BTGreaterStrategyNumber - 1] = NULL;
				}
			}

			/*
			 * 将清理后的键放入 outkeys[] 数组中，然后
			 * 标记它们是否为必需。如果在此之前的所有属性都有“=”，
			 * 则它们是必需的（可能只在一个方向上）。
			 */
			for (fc_j = BTMaxStrategyNumber; --fc_j >= 0;)
			{
				if (fc_xform[fc_j])
				{
					ScanKey		fc_outkey = &fc_outkeys[fc_new_numberOfKeys++];

					memcpy(fc_outkey, fc_xform[fc_j], sizeof(ScanKeyData));
					if (fc_priorNumberOfEqualCols == fc_attno - 1)
						fc__bt_mark_scankey_required(fc_outkey);
				}
			}

			/*
			 * 如果完成则在此退出循环。
			 */
			if (fc_i == fc_numberOfKeys)
				break;

			/* 为新 attno 重新初始化 */
			fc_attno = fc_cur->sk_attno;
			memset(fc_xform, 0, sizeof(fc_xform));
		}

		/* 检查该键的操作员对应的策略 */
		fc_j = fc_cur->sk_strategy - 1;

		/* 如果是行比较，则直接将其推送到输出数组 */
		if (fc_cur->sk_flags & SK_ROW_HEADER)
		{
			ScanKey		fc_outkey = &fc_outkeys[fc_new_numberOfKeys++];

			memcpy(fc_outkey, fc_cur, sizeof(ScanKeyData));
			if (fc_numberOfEqualCols == fc_attno - 1)
				fc__bt_mark_scankey_required(fc_outkey);

			/*
			 * 我们不支持使用等式的 RowCompare；这样的条件会
			 * 破坏 numberOfEqualCols 的跟踪。
			 */
			Assert(fc_j != (BTEqualStrategyNumber - 1));
			continue;
		}

		/* 我们以前见过其中一个吗？ */
		if (fc_xform[fc_j] == NULL)
		{
			/* 没有，所以记住这个 scankey */
			fc_xform[fc_j] = fc_cur;
		}
		else
		{
			/* 是的，只保留更严格的键 */
			if (fc__bt_compare_scankey_args(fc_scan, fc_cur, fc_cur, fc_xform[fc_j],
										 &fc_test_result))
			{
				if (fc_test_result)
					fc_xform[fc_j] = fc_cur;
				else if (fc_j == (BTEqualStrategyNumber - 1))
				{
					/* key == a && key == b，但 a != b */
					fc_so->qual_ok = false;
					return;
				}
				/* 否则旧键更严格，保留它 */
			}
			else
			{
				/*
				 * 我们无法确定哪个键更具限制性。保留
				 * xform[j]中的前一个，并将这个直接推送到
				 * 输出数组中。
				 */
				ScanKey		fc_outkey = &fc_outkeys[fc_new_numberOfKeys++];

				memcpy(fc_outkey, fc_cur, sizeof(ScanKeyData));
				if (fc_numberOfEqualCols == fc_attno - 1)
					fc__bt_mark_scankey_required(fc_outkey);
			}
		}
	}

	fc_so->numberOfKeys = fc_new_numberOfKeys;
}

/*
 * 使用指定操作符比较两个 scankey 值。
 *
 * 我们想要执行的测试在逻辑上是“leftarg op rightarg”，其中
 * leftarg 和 rightarg 是这些 ScanKeys 中的 sk_argument 值，而
 * 比较操作符是 op ScanKey 中的那个。然而，在跨数据类型的情况下，
 * 我们可能需要在索引的 opfamily 中查找正确的操作符：它是
 * amopstrategy = op->sk_strategy 且 amoplefttype/amoprighttype
 * 等于两个参数数据类型的操作符。
 *
 * 如果 opfamily 没有提供一整套跨类型操作符，我们可能无法
 * 进行比较。如果我们可以进行比较，我们将操作符结果存储在
 * *result 中并返回 true。如果无法进行比较，我们返回 false。
 *
 * 注意：op 总是指向与 leftarg 或 rightarg 相同的 ScanKey。
 * 由于我们不在 scankeys 上涂抹，这种别名应该不会造成任何
 * 麻烦。
 *
 * 注意：这个例程需要对应用于索引列的任何 DESC 选项不敏感。
 * 例如，“x < 4”是比“x < 5”更紧的约束，无论索引是如何排序的。
 */
static bool fc__bt_compare_scankey_args(IndexScanDesc fc_scan, ScanKey fc_op,
						 ScanKey fc_leftarg, ScanKey fc_rightarg,
						 bool *fc_result)
{
	Relation	fc_rel = fc_scan->indexRelation;
	Oid			fc_lefttype,
				fc_righttype,
				fc_optype,
				fc_opcintype,
				fc_cmp_op;
	StrategyNumber fc_strat;

	/*
	 * 首先，处理一个或两个参数为 NULL 的情况。这应该
	 * 只发生在 scankeys 表示 IS NULL/NOT NULL 条件时。
	 */
	if ((fc_leftarg->sk_flags | fc_rightarg->sk_flags) & SK_ISNULL)
	{
		bool		fc_leftnull,
					fc_rightnull;

		if (fc_leftarg->sk_flags & SK_ISNULL)
		{
			Assert(fc_leftarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
			fc_leftnull = true;
		}
		else
			fc_leftnull = false;
		if (fc_rightarg->sk_flags & SK_ISNULL)
		{
			Assert(fc_rightarg->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL));
			fc_rightnull = true;
		}
		else
			fc_rightnull = false;

		/*
		 * 我们将 NULL 视为大于或小于所有其他值。
		 * 由于 true > false，下面的测试在 NULLS LAST
		 * 逻辑中能够正确工作。如果索引是 NULLS FIRST，
		 * 我们需要翻转策略。
		 */
		fc_strat = fc_op->sk_strategy;
		if (fc_op->sk_flags & SK_BT_NULLS_FIRST)
			fc_strat = BTCommuteStrategyNumber(fc_strat);

		switch (fc_strat)
		{
			case BTLessStrategyNumber:
				*fc_result = (fc_leftnull < fc_rightnull);
				break;
			case BTLessEqualStrategyNumber:
				*fc_result = (fc_leftnull <= fc_rightnull);
				break;
			case BTEqualStrategyNumber:
				*fc_result = (fc_leftnull == fc_rightnull);
				break;
			case BTGreaterEqualStrategyNumber:
				*fc_result = (fc_leftnull >= fc_rightnull);
				break;
			case BTGreaterStrategyNumber:
				*fc_result = (fc_leftnull > fc_rightnull);
				break;
			default:
				elog(ERROR, "unrecognized StrategyNumber: %d", (int) fc_strat);
				*fc_result = false;	/* 保持编译器安静 */
				break;
		}
		return true;
	}

	/*
	 * 我们需要担心的 opfamily 由索引列确定。
	 */
	Assert(fc_leftarg->sk_attno == fc_rightarg->sk_attno);

	fc_opcintype = fc_rel->rd_opcintype[fc_leftarg->sk_attno - 1];

	/*
	 * 确定 ScanKey 参数的实际数据类型。我们必须
	 * 支持 sk_subtype == InvalidOid 意味着 opclass
	 * 输入类型的约定；这是为了简化 ScanKeyInit() 的工作。
	 */
	fc_lefttype = fc_leftarg->sk_subtype;
	if (fc_lefttype == InvalidOid)
		fc_lefttype = fc_opcintype;
	fc_righttype = fc_rightarg->sk_subtype;
	if (fc_righttype == InvalidOid)
		fc_righttype = fc_opcintype;
	fc_optype = fc_op->sk_subtype;
	if (fc_optype == InvalidOid)
		fc_optype = fc_opcintype;

	/*
	 * 如果 leftarg 和 rightarg 与“op” scankey
	 * 期望的类型匹配，我们可以使用其已经查找过的比较函数。
	 */
	if (fc_lefttype == fc_opcintype && fc_righttype == fc_optype)
	{
		*fc_result = DatumGetBool(FunctionCall2Coll(&fc_op->sk_func,
												 fc_op->sk_collation,
												 fc_leftarg->sk_argument,
												 fc_rightarg->sk_argument));
		return true;
	}

	/*
	 * 否则，我们需要去 syscache 寻找合适的操作符。
	 * （这不会导致无限递归，因为通过 syscache 查找
	 * 启动的任何索引扫描都不会使用跨数据类型
	 * 操作符。）
	 *
	 * 如果 sk_strategy 被 _bt_fix_scankey_strategy 翻转，
	 * 我们必须将其恢复，以获取正确的 opfamily 成员。
	 */
	fc_strat = fc_op->sk_strategy;
	if (fc_op->sk_flags & SK_BT_DESC)
		fc_strat = BTCommuteStrategyNumber(fc_strat);

	fc_cmp_op = get_opfamily_member(fc_rel->rd_opfamily[fc_leftarg->sk_attno - 1],
								 fc_lefttype,
								 fc_righttype,
								 fc_strat);
	if (OidIsValid(fc_cmp_op))
	{
		RegProcedure fc_cmp_proc = get_opcode(fc_cmp_op);

		if (RegProcedureIsValid(fc_cmp_proc))
		{
			*fc_result = DatumGetBool(OidFunctionCall2Coll(fc_cmp_proc,
														fc_op->sk_collation,
														fc_leftarg->sk_argument,
														fc_rightarg->sk_argument));
			return true;
		}
	}

	/* 无法进行比较 */
	*fc_result = false;			/* 抑制编译器警告 */
	return false;
}

/*
 * 根据 indoptions 的需要调整 scankey 的策略和标志设置。
 *
 * 我们将适当的 indoption 值复制到 scankey sk_flags 中
 * （通过移位以避免覆盖系统定义的标志位）。此外，如果
 * 设置了 DESC 选项，则交换（翻转）操作符策略号。
 *
 * 次要目的在于检查 IS NULL/NOT NULL scankeys，并为它们正确设置
 * 策略字段。
 *
 * 最后，对于普通的 scankeys（不包括 IS NULL/NOT NULL），我们检查
 * 是否有 NULL 比较值。由于所有 btree 操作符都被认为是严格的，
 * NULL 意味着条件无法满足。如果比较值不是 NULL，则返回 true，
 * 如果扫描应该放弃，则返回 false。
 *
 * 此函数适用于 *输入* scankey 结构；因此
 * 在重新扫描时，我们将查看已处理的 scankeys。因此
 * 我们必须小心不要重新交换策略，如果我们已经这样做过的话。
 * 修改调用者的 scankey 副本有点麻烦，但实际上
 * 不应该有任何问题，因为在 scankey 存在期间，索引的 indoptions
 * 肯定不会改变。
 */
static bool fc__bt_fix_scankey_strategy(ScanKey fc_skey, int16 *fc_indoption)
{
	int			fc_addflags;

	fc_addflags = fc_indoption[fc_skey->sk_attno - 1] << SK_BT_INDOPTION_SHIFT;

	/*
	 * 我们将所有 btree 操作符视为严格的（即使它们在 pg_proc 中没有这样标记）。
	 * 这意味着带有 NULL 比较常量的操作符条件不可能成功，
	 * 我们可以立即拒绝它。
	 *
	 * 但是，我们现在也支持 "x IS NULL" 子句作为搜索条件，
	 * 因此在这种情况下继续进行。规划器在这种情况下没有填入
	 * 任何特定的策略，因此将其设置为 BTEqualStrategyNumber 
	 * —— 我们可以在搜索策略中将 IS NULL 视为等于操作符。
	 *
	 * 同样，"x IS NOT NULL" 也被支持。我们将其视为在 NULLS LAST
	 * 索引中“小于 NULL”，或在 NULLS FIRST 索引中“大于 NULL”。
	 *
	 * 注意：未来我们可能需要从索引列的排序规则填写 sk_collation。
	 * 目前来说这是一个非问题，因为我们永远不会在 NULL 上实际调用比较操作符。
	 */
	if (fc_skey->sk_flags & SK_ISNULL)
	{
		/* SK_ISNULL 不应该在行头 scankey 中设置 */
		Assert(!(fc_skey->sk_flags & SK_ROW_HEADER));

		/* 在 scankey 中设置 indoption 标志（可能已经完成） */
		fc_skey->sk_flags |= fc_addflags;

		/* 为 IS NULL 或 NOT NULL 查询设置正确的策略 */
		if (fc_skey->sk_flags & SK_SEARCHNULL)
		{
			fc_skey->sk_strategy = BTEqualStrategyNumber;
			fc_skey->sk_subtype = InvalidOid;
			fc_skey->sk_collation = InvalidOid;
		}
		else if (fc_skey->sk_flags & SK_SEARCHNOTNULL)
		{
			if (fc_skey->sk_flags & SK_BT_NULLS_FIRST)
				fc_skey->sk_strategy = BTGreaterStrategyNumber;
			else
				fc_skey->sk_strategy = BTLessStrategyNumber;
			fc_skey->sk_subtype = InvalidOid;
			fc_skey->sk_collation = InvalidOid;
		}
		else
		{
			/* 普通的条件，因此无法满足 */
			return false;
		}

		/* 不需要做其余的 */
		return true;
	}

	/* 如果我们还没有这样做，则为 DESC 调整策略 */
	if ((fc_addflags & SK_BT_DESC) && !(fc_skey->sk_flags & SK_BT_DESC))
		fc_skey->sk_strategy = BTCommuteStrategyNumber(fc_skey->sk_strategy);
	fc_skey->sk_flags |= fc_addflags;

	/* 如果是行头，则以类似方式修复行成员标志和策略 */
	if (fc_skey->sk_flags & SK_ROW_HEADER)
	{
		ScanKey		fc_subkey = (ScanKey) DatumGetPointer(fc_skey->sk_argument);

		for (;;)
		{
			Assert(fc_subkey->sk_flags & SK_ROW_MEMBER);
			fc_addflags = fc_indoption[fc_subkey->sk_attno - 1] << SK_BT_INDOPTION_SHIFT;
			if ((fc_addflags & SK_BT_DESC) && !(fc_subkey->sk_flags & SK_BT_DESC))
				fc_subkey->sk_strategy = BTCommuteStrategyNumber(fc_subkey->sk_strategy);
			fc_subkey->sk_flags |= fc_addflags;
			if (fc_subkey->sk_flags & SK_ROW_END)
				break;
			fc_subkey++;
		}
	}

	return true;
}

/*
 * 标记一个扫描键为“继续扫描所需”。
 *
 * 根据操作符类型，可能需要该键在两个扫描方向上都存在或仅在一个方向上存在。此外，如果该键是行比较头，我们必须将其第一个附属ScanKey标记为必需。(后续附属ScanKeys通常用于低位列，因此不能被要求，因为它们在第一个非等式扫描键之后。)
 *
 * 注意：当我们在附属扫描键中设置必需键标志位时，我们是在修改一个属于索引AM调用者的数据结构，而不是我们的私有副本。这应该没问题，因为标记不会因查询中的扫描而改变，因此我们无论如何在重新扫描时都将以相同方式重新标记。不过，这需要注意。
 */
static void fc__bt_mark_scankey_required(ScanKey fc_skey)
{
	int			fc_addflags;

	switch (fc_skey->sk_strategy)
	{
		case BTLessStrategyNumber:
		case BTLessEqualStrategyNumber:
			fc_addflags = SK_BT_REQFWD;
			break;
		case BTEqualStrategyNumber:
			fc_addflags = SK_BT_REQFWD | SK_BT_REQBKWD;
			break;
		case BTGreaterEqualStrategyNumber:
		case BTGreaterStrategyNumber:
			fc_addflags = SK_BT_REQBKWD;
			break;
		default:
			elog(ERROR, "unrecognized StrategyNumber: %d",
				 (int) fc_skey->sk_strategy);
			fc_addflags = 0;		/* 保持编译器安静 */
			break;
	}

	fc_skey->sk_flags |= fc_addflags;

	if (fc_skey->sk_flags & SK_ROW_HEADER)
	{
		ScanKey		fc_subkey = (ScanKey) DatumGetPointer(fc_skey->sk_argument);

		/* 第一个子键应该与头部相同的列/操作符 */
		Assert(fc_subkey->sk_flags & SK_ROW_MEMBER);
		Assert(fc_subkey->sk_attno == fc_skey->sk_attno);
		Assert(fc_subkey->sk_strategy == fc_skey->sk_strategy);
		fc_subkey->sk_flags |= fc_addflags;
	}
}

/*
 * 测试一个索引元组是否满足所有扫描键条件。
 *
 * 如果满足条件则返回真，不满足则返回假。如果元组未通过该条件，我们还将确定是否需要继续扫描超出该元组，并相应地设置*continuescan。请参阅上面有关如何做到这一点的_bt_preprocess_keys()注释。
 *
 * 前向扫描调用者可以传递一个高键元组，希望我们将*continuescan设为假，从而避免不必要访问右侧的页面。
 *
 * scan：索引扫描描述符（包含一个搜索类型的扫描键）
 * tuple：待测试的索引元组
 * tupnatts：tupnatts中的属性数量（高键可能被截断）
 * dir：我们正在扫描的方向
 * continuescan：输出参数（在所有情况下都将正确设置）
 */
bool _bt_checkkeys(IndexScanDesc fc_scan, IndexTuple fc_tuple, int fc_tupnatts,
			  ScanDirection fc_dir, bool *fc_continuescan)
{
	TupleDesc	fc_tupdesc;
	BTScanOpaque fc_so;
	int			fc_keysz;
	int			fc_ikey;
	ScanKey		fc_key;

	Assert(BTreeTupleGetNAtts(fc_tuple, fc_scan->indexRelation) == fc_tupnatts);

	*fc_continuescan = true;		/* 默认假设 */

	fc_tupdesc = RelationGetDescr(fc_scan->indexRelation);
	fc_so = (BTScanOpaque) fc_scan->opaque;
	fc_keysz = fc_so->numberOfKeys;

	for (fc_key = fc_so->keyData, fc_ikey = 0; fc_ikey < fc_keysz; fc_key++, fc_ikey++)
	{
		Datum		fc_datum;
		bool		fc_isNull;
		Datum		fc_test;

		if (fc_key->sk_attno > fc_tupnatts)
		{
			/*
			 * 该属性被截断（必须是高键）。右侧页面上第一个非支点元组中
			 * 该属性的值可能是任何可能的值。假设截断
			 * 属性通过了条件。
			 */
			Assert(ScanDirectionIsForward(fc_dir));
			Assert(BTreeTupleIsPivot(fc_tuple));
			continue;
		}

		/* 行比较键需要特殊处理 */
		if (fc_key->sk_flags & SK_ROW_HEADER)
		{
			if (fc__bt_check_rowcompare(fc_key, fc_tuple, fc_tupnatts, fc_tupdesc, fc_dir,
									 fc_continuescan))
				continue;
			return false;
		}

		fc_datum = index_getattr(fc_tuple,
							  fc_key->sk_attno,
							  fc_tupdesc,
							  &fc_isNull);

		if (fc_key->sk_flags & SK_ISNULL)
		{
			/* 处理IS NULL/NOT NULL测试 */
			if (fc_key->sk_flags & SK_SEARCHNULL)
			{
				if (fc_isNull)
					continue;	/* 元组满足此条件 */
			}
			else
			{
				Assert(fc_key->sk_flags & SK_SEARCHNOTNULL);
				if (!fc_isNull)
					continue;	/* 元组满足此条件 */
			}

			/*
			 * 元组未通过此条件。如果这是当前扫描方向的必需条件，
			 * 那么我们可以得出结论，不会有更多元组通过。
			 */
			if ((fc_key->sk_flags & SK_BT_REQFWD) &&
				ScanDirectionIsForward(fc_dir))
				*fc_continuescan = false;
			else if ((fc_key->sk_flags & SK_BT_REQBKWD) &&
					 ScanDirectionIsBackward(fc_dir))
				*fc_continuescan = false;

			/*
			 * 无论如何，这个索引元组与条件不匹配。
			 */
			return false;
		}

		if (fc_isNull)
		{
			if (fc_key->sk_flags & SK_BT_NULLS_FIRST)
			{
				/*
				 * 由于NULL在非NULL之前排序，我们知道已达到该
				 * 索引属性值范围的下限。在向后扫描时，
				 * 如果该条件是“必须匹配”子集之一，则可以停止。
				 * 无论条件是>还是<，只要是必需的，就可以停止，
				 * 因为不可能有未来的元组会通过。但是在
				 * 向前扫描时，我们必须继续，因为我们可能
				 * 已经最初定位到索引的起始位置。
				 */
				if ((fc_key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
					ScanDirectionIsBackward(fc_dir))
					*fc_continuescan = false;
			}
			else
			{
				/*
				 * 由于NULL值在非NULL值之后排序，我们知道我们已达到该
				 * 索引属性值范围的上限。在向前扫描中，如果该条件是
				 * “必匹配”子集之一，我们可以停止。无论该条件是>
				 * 还是<，只要它是必需的，我们都可以停止，因为不
				 * 可能有任何将来元组可以通过。然而，在向后扫描时，
				 * 我们必须继续，因为我们可能最初已定位到索引的末尾。
				 */
				if ((fc_key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
					ScanDirectionIsForward(fc_dir))
					*fc_continuescan = false;
			}

			/*
			 * 无论如何，这个索引元组与条件不匹配。
			 */
			return false;
		}

		fc_test = FunctionCall2Coll(&fc_key->sk_func, fc_key->sk_collation,
								 fc_datum, fc_key->sk_argument);

		if (!DatumGetBool(fc_test))
		{
			/*
			 * 元组未通过此条件。如果这是当前扫描方向的必需
			 * 条件，则我们可以得出结论，没有更多元组将会通过。
			 *
			 * 注意：因为我们在任何必需的相等条件失败时立
			 * 即停止扫描，所以在可用时在_bt_first()中
			 * 使用相等条件进行初始定位是至关重要的。请参见
			 * _bt_first()中的注释。
			 */
			if ((fc_key->sk_flags & SK_BT_REQFWD) &&
				ScanDirectionIsForward(fc_dir))
				*fc_continuescan = false;
			else if ((fc_key->sk_flags & SK_BT_REQBKWD) &&
					 ScanDirectionIsBackward(fc_dir))
				*fc_continuescan = false;

			/*
			 * 无论如何，这个索引元组与条件不匹配。
			 */
			return false;
		}
	}

	/* 如果我们走到这里，元组通过了所有索引条件。 */
	return true;
}

/*
 * 测试一个索引元组是否满足行比较扫描条件。
 *
 * 如果满足则返回true，如果不满足则返回false。如果不满足，
 * 还清除*continuescan，如果在当前扫描方向上无法
 * 有任何将来元组通过该条件。
 *
 * 这是_bt_checkkeys的一个子例程，更多信息请参见之。
 */
static bool fc__bt_check_rowcompare(ScanKey fc_skey, IndexTuple fc_tuple, int fc_tupnatts,
					 TupleDesc fc_tupdesc, ScanDirection fc_dir, bool *fc_continuescan)
{
	ScanKey		fc_subkey = (ScanKey) DatumGetPointer(fc_skey->sk_argument);
	int32		fc_cmpresult = 0;
	bool		fc_result;

	/* 第一个子键应该与头部所示的相同 */
	Assert(fc_subkey->sk_attno == fc_skey->sk_attno);

	/* 遍历行条件的列 */
	for (;;)
	{
		Datum		fc_datum;
		bool		fc_isNull;

		Assert(fc_subkey->sk_flags & SK_ROW_MEMBER);

		if (fc_subkey->sk_attno > fc_tupnatts)
		{
			/*
			 * 该属性被截断（必须是高键）。右侧页面上第一个非支点元组中
			 * 该属性的值可能是任何可能的值。假设截断
			 * 属性通过了条件。
			 */
			Assert(ScanDirectionIsForward(fc_dir));
			Assert(BTreeTupleIsPivot(fc_tuple));
			fc_cmpresult = 0;
			if (fc_subkey->sk_flags & SK_ROW_END)
				break;
			fc_subkey++;
			continue;
		}

		fc_datum = index_getattr(fc_tuple,
							  fc_subkey->sk_attno,
							  fc_tupdesc,
							  &fc_isNull);

		if (fc_isNull)
		{
			if (fc_subkey->sk_flags & SK_BT_NULLS_FIRST)
			{
				/*
				 * 由于NULL在非NULL之前排序，我们知道已达到该
				 * 索引属性值范围的下限。在向后扫描时，
				 * 如果该条件是“必须匹配”子集之一，则可以停止。
				 * 无论条件是>还是<，只要是必需的，就可以停止，
				 * 因为不可能有未来的元组会通过。但是在
				 * 向前扫描时，我们必须继续，因为我们可能
				 * 已经最初定位到索引的起始位置。
				 */
				if ((fc_subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
					ScanDirectionIsBackward(fc_dir))
					*fc_continuescan = false;
			}
			else
			{
				/*
				 * 由于NULL值在非NULL值之后排序，我们知道我们已达到该
				 * 索引属性值范围的上限。在向前扫描中，如果该条件是
				 * “必匹配”子集之一，我们可以停止。无论该条件是>
				 * 还是<，只要它是必需的，我们都可以停止，因为不
				 * 可能有任何将来元组可以通过。然而，在向后扫描时，
				 * 我们必须继续，因为我们可能最初已定位到索引的末尾。
				 */
				if ((fc_subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
					ScanDirectionIsForward(fc_dir))
					*fc_continuescan = false;
			}

			/*
			 * 无论如何，这个索引元组与条件不匹配。
			 */
			return false;
		}

		if (fc_subkey->sk_flags & SK_ISNULL)
		{
			/*
			 * 与简单的扫描键情况不同，这不是一个不允许的情况。
			 * 但它永远无法匹配。如果所有之前的行比较
			 * 列都是扫描方向所必需的，我们可以停止
			 * 扫描，因为不可能还有其他元组会成功。
			 */
			if (fc_subkey != (ScanKey) DatumGetPointer(fc_skey->sk_argument))
				fc_subkey--;
			if ((fc_subkey->sk_flags & SK_BT_REQFWD) &&
				ScanDirectionIsForward(fc_dir))
				*fc_continuescan = false;
			else if ((fc_subkey->sk_flags & SK_BT_REQBKWD) &&
					 ScanDirectionIsBackward(fc_dir))
				*fc_continuescan = false;
			return false;
		}

		/* 执行测试 --- 三方比较而不是布尔运算符 */
		fc_cmpresult = DatumGetInt32(FunctionCall2Coll(&fc_subkey->sk_func,
													fc_subkey->sk_collation,
													fc_datum,
													fc_subkey->sk_argument));

		if (fc_subkey->sk_flags & SK_BT_DESC)
			INVERT_COMPARE_RESULT(fc_cmpresult);

		/* 如果不相等，则完成比较，否则推进到下一列 */
		if (fc_cmpresult != 0)
			break;

		if (fc_subkey->sk_flags & SK_ROW_END)
			break;
		fc_subkey++;
	}

	/*
	 * 此时cmpresult表示行
	 * 比较的总体结果，subkey指向决定列
	 * （如果结果是“=”则指向最后一列）。
	 */
	switch (fc_subkey->sk_strategy)
	{
			/* EQ和NE情况在这里不被允许 */
		case BTLessStrategyNumber:
			fc_result = (fc_cmpresult < 0);
			break;
		case BTLessEqualStrategyNumber:
			fc_result = (fc_cmpresult <= 0);
			break;
		case BTGreaterEqualStrategyNumber:
			fc_result = (fc_cmpresult >= 0);
			break;
		case BTGreaterStrategyNumber:
			fc_result = (fc_cmpresult > 0);
			break;
		default:
			elog(ERROR, "unrecognized RowCompareType: %d",
				 (int) fc_subkey->sk_strategy);
			fc_result = 0;			/* 保持编译器安静 */
			break;
	}

	if (!fc_result)
	{
		/*
		 * 元组未通过此条件。如果这是当前
		 * 扫描方向的必需条件，那么我们可以得出没有
		 * 进一步元组会通过的结论。
		 * 请注意，我们必须查看决定列，而不 
		 * 一定是行条件的第一列或最后一列。
		 */
		if ((fc_subkey->sk_flags & SK_BT_REQFWD) &&
			ScanDirectionIsForward(fc_dir))
			*fc_continuescan = false;
		else if ((fc_subkey->sk_flags & SK_BT_REQBKWD) &&
				 ScanDirectionIsBackward(fc_dir))
			*fc_continuescan = false;
	}

	return fc_result;
}

/*
 * _bt_killitems - 为索引扫描调用者告诉我们已删除的项设置LP_DEAD状态
 *
 * scan->opaque，通过so本地引用，包含关于当前页面和
 * 被删除元组的信息（通常，这应该只在so->numKilled > 0时调用）。
 *
 * 调用者没有锁定页面，并且可能在缓冲区中未固定
 * 该页面。请注意，读取锁足以设置LP_DEAD状态
 * （这只是一个提示）。
 *
 * 我们通过堆TID匹配项目，然后假设它们是要删除的正确项。
 * 我们处理由于插入导致项目右移的情况。
 * 如果由于拆分而导致项目从当前页面移开，我们将无法
 * 找到它而不采取任何行动（这不是错误情况 —— 我们假设该项目
 * 最终会在未来的索引扫描中被标记）。
 *
 * 请注意，如果我们从最初读取项目开始，
 * 在应用此功能之前一直保持对目标页面的固定，
 * VACUUM不能从页面中删除任何项目，因此无需从
 * 记录的偏移量向左搜索。（这个观察还保证该项目仍然
 * 是要删除的正确项，否则可能会有疑问，因为堆
 * TID可能会被回收。）即使页面已被
 * 插入和页面拆分修改，这也适用，因此无需查阅LSN。
 *
 * 如果在读取页面后释放了固定，那么我们重新读取它。如果它
 * 自我们读取以来已被修改（由LSN确定），我们不敢
 * 标记任何条目，因为旧的条目可能已被清除，
 * TID可能已被完全不同的堆元组重新使用。
 */
void _bt_killitems(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	OffsetNumber fc_minoff;
	OffsetNumber fc_maxoff;
	int			fc_i;
	int			fc_numKilled = fc_so->numKilled;
	bool		fc_killedsomething = false;
	bool		fc_droppedpin PG_USED_FOR_ASSERTS_ONLY;

	Assert(BTScanPosIsValid(fc_so->currPos));

	/*
	 * 始终重置扫描状态，以便我们不在其他页面上查找相同的项目。
	 */
	fc_so->numKilled = 0;

	if (BTScanPosIsPinned(fc_so->currPos))
	{
		/*
		 * 自从我们读取索引元组以来，我们一直保持对此页面的固定，
		 * 所以我们所需要做的就是锁定它。固定将防止
		 * 页面上任何TID的重复使用，因此无需检查
		 * LSN。
		 */
		fc_droppedpin = false;
		_bt_lockbuf(fc_scan->indexRelation, fc_so->currPos.buf, BT_READ);

		fc_page = BufferGetPage(fc_so->currPos.buf);
	}
	else
	{
		Buffer		fc_buf;

		fc_droppedpin = true;
		/* 尝试重新读取缓冲区，获取固定和锁定。 */
		fc_buf = _bt_getbuf(fc_scan->indexRelation, fc_so->currPos.currPage, BT_READ);

		fc_page = BufferGetPage(fc_buf);
		if (BufferGetLSNAtomic(fc_buf) == fc_so->currPos.lsn)
			fc_so->currPos.buf = fc_buf;
		else
		{
			/* 修改后未固定意味着提示不安全。 */
			_bt_relbuf(fc_scan->indexRelation, fc_buf);
			return;
		}
	}

	fc_opaque = BTPageGetOpaque(fc_page);
	fc_minoff = P_FIRSTDATAKEY(fc_opaque);
	fc_maxoff = PageGetMaxOffsetNumber(fc_page);

	for (fc_i = 0; fc_i < fc_numKilled; fc_i++)
	{
		int			fc_itemIndex = fc_so->killedItems[fc_i];
		BTScanPosItem *fc_kitem = &fc_so->currPos.items[fc_itemIndex];
		OffsetNumber fc_offnum = fc_kitem->indexOffset;

		Assert(fc_itemIndex >= fc_so->currPos.firstItem &&
			   fc_itemIndex <= fc_so->currPos.lastItem);
		if (fc_offnum < fc_minoff)
			continue;			/* 纯粹的偏执 */
		while (fc_offnum <= fc_maxoff)
		{
			ItemId		fc_iid = PageGetItemId(fc_page, fc_offnum);
			IndexTuple	fc_ituple = (IndexTuple) PageGetItem(fc_page, fc_iid);
			bool		fc_killtuple = false;

			if (BTreeTupleIsPosting(fc_ituple))
			{
				int			fc_pi = fc_i + 1;
				int			fc_nposting = BTreeTupleGetNPosting(fc_ituple);
				int			fc_j;

				/*
				 * 我们依赖约定，即在扫描位置
				 * 项数组中，堆 TID 是按升序存储的，
				 * 用于最初来自一个发布列表
				 * 元组的一组 TID。这个约定甚至适用于
				 * 向后扫描，虽然返回 TID 的降序可能
				 * 看起来更自然。这是关于有效性，而不是
				 * 正确性。
				 *
				 * 请注意，自我们第一次读取它以来，
				 * 页面可能已经以几乎任何方式修改（在
				 * !droppedpin 的情况下），因此这
				 * 个发布列表元组在我们第一次遇到其
				 * 堆 TID 时可能不是一个发布列表元组。
				 */
				for (fc_j = 0; fc_j < fc_nposting; fc_j++)
				{
					ItemPointer fc_item = BTreeTupleGetPostingN(fc_ituple, fc_j);

					if (!ItemPointerEquals(fc_item, &fc_kitem->heapTid))
						break;	/* 退出发布列表循环 */

					/*
					 * 当堆 TID 匹配时，kitem 必须
					 * 具有匹配的 offnum，尽管仅在
					 * 页面不能被并发修改的常见情况下。
					 */
					Assert(fc_kitem->indexOffset == fc_offnum || !fc_droppedpin);

					/*
					 * 在这里进行后续 kitem 的预读。
					 *
					 * 我们依赖以下假设：在这里不推进 kitem
					 * 将防止我们在下一次循环迭代中通过
					 * 不匹配其下一个堆 TID 来将发布列表
					 * 元组视为完全死掉。
					 *
					 * 另一方面，如果这是发布列表元组中的
					 * 最终堆 TID，那么元组将被杀死，
					 * 无论如何（即我们正确处理最后一个
					 * kitem 也是最后一个索引元组中的
					 * 最后一个堆 TID 的情况 - 发布元组
					 * 仍然被杀死）。
					 */
					if (fc_pi < fc_numKilled)
						fc_kitem = &fc_so->currPos.items[fc_so->killedItems[fc_pi++]];
				}

				/*
				 * 不必推进最外层循环的整数迭代器，
				 * 以避免处理与相同的 offnum/发布列表元组
				 * 相关的被杀死的项。这种微优化似乎
				 * 并不值得。（最外层循环的进一步迭代
				 * 将不会匹配该发布列表的第一个堆
				 * TID，因此我们将相当快地推进到下一个
				 * offnum/索引元组。）
				 */
				if (fc_j == fc_nposting)
					fc_killtuple = true;
			}
			else if (ItemPointerEquals(&fc_ituple->t_tid, &fc_kitem->heapTid))
				fc_killtuple = true;

			/*
			 * 如果索引项尚未标记为死，则将其标记为死。
			 * 由于在持有可能处于共享模式的缓冲锁时发生
			 * 这种情况，因此多个进程可能会尝试同时执行
			 * 此操作，导致多个完整页面图像被发送到
			 * WAL（如果 wal_log_hints 或数据校验和被启用），
			 * 这是不希望发生的。
			 */
			if (fc_killtuple && !ItemIdIsDead(fc_iid))
			{
				/* 找到项/所有发布列表项 */
				ItemIdMarkDead(fc_iid);
				fc_killedsomething = true;
				break;			/* 退出内部搜索循环 */
			}
			fc_offnum = OffsetNumberNext(fc_offnum);
		}
	}

	/*
	 * 由于如果需要，可以稍后重新执行此操作，
	 * 因此将其标记为脏提示。
	 *
	 * 每当我们标记任何内容为 LP_DEAD 时，
	 * 我们也会设置页面的 BTP_HAS_GARBAGE
	 * 标志，这同样只是一个提示。（请注意，
	 * 我们仅在 !heapkeyspace 索引中依赖页面级标志。）
	 */
	if (fc_killedsomething)
	{
		fc_opaque->btpo_flags |= BTP_HAS_GARBAGE;
		MarkBufferDirtyHint(fc_so->currPos.buf, true);
	}

	_bt_unlockbuf(fc_scan->indexRelation, fc_so->currPos.buf);
}


/*
 * 以下例程管理一个共享内存区域，在其中我们追踪
 * "vacuum cycle IDs" 分配给当前活跃的 btree vacuuming
 * 操作。每次开始一个 vacuum 时，会增加一个计数器
 * 以分配一个 cycle ID。由于多个 vacuum 可能同时
 * 活跃，我们必须追踪每个活跃 vacuum 的 cycle ID；
 * 这最多需要 MaxBackends 个条目（通常要少得多）。
 * 我们假设对于给定索引最多只能有一个 vacuum 活跃。
 *
 * 对共享内存区域的访问由 BtreeVacuumLock 控制。
 * 原则上，我们可以为每个索引使用一个单独的 lmgr locktag，
 * 但一个 LWLock 成本更低，考虑到锁被持有的时间非常短，
 * 并发的影响应该是微不足道的。
 */

typedef struct BTOneVacInfo
{
	LockRelId	relid;			/* 索引的全局标识符 */
	BTCycleId	cycleid;		/* 其活跃 VACUUM 的 cycle ID */
} BTOneVacInfo;

typedef struct BTVacInfo
{
	BTCycleId	cycle_ctr;		/* 最近分配的 cycle ID */
	int			num_vacuums;	/* 当前活跃的 VACUUM 数量 */
	int			max_vacuums;	/* vacuums[] 数组的分配长度 */
	BTOneVacInfo vacuums[FLEXIBLE_ARRAY_MEMBER];
} BTVacInfo;

static BTVacInfo *btvacinfo;


/*
 * _bt_vacuum_cycleid --- 获取索引的活跃 vacuum cycle ID，
 *		如果没有活跃 VACUUM，则返回零
 *
 * 注意：为了正确的互锁，调用者必须已经在每个将
 * 存储 cycle ID 的缓冲区上持有 pin 和独占锁。
 * 这确保即使 VACUUM 紧接着开始，它也无法
 * 处理这些页面，直到页面分裂完成。
 */
BTCycleId
_bt_vacuum_cycleid(Relation fc_rel)
{
	BTCycleId	fc_result = 0;
	int			fc_i;

	/* 共享锁足够，因为这是只读操作 */
	LWLockAcquire(BtreeVacuumLock, LW_SHARED);

	for (fc_i = 0; fc_i < btvacinfo->num_vacuums; fc_i++)
	{
		BTOneVacInfo *fc_vac = &btvacinfo->vacuums[fc_i];

		if (fc_vac->relid.relId == fc_rel->rd_lockInfo.lockRelId.relId &&
			fc_vac->relid.dbId == fc_rel->rd_lockInfo.lockRelId.dbId)
		{
			fc_result = fc_vac->cycleid;
			break;
		}
	}

	LWLockRelease(BtreeVacuumLock);
	return fc_result;
}

/*
 * _bt_start_vacuum --- 将 cycle ID 分配给刚开始的 VACUUM 操作
 *
 * 注意：调用者必须保证最终会调用
 * _bt_end_vacuum，否则我们将永久泄漏一个数组槽位。
 * 为了确保即使在 elog(FATAL) 场景中也发生这种情况，
 * 适当的编码不仅仅是 PG_TRY，而是
 *		PG_ENSURE_ERROR_CLEANUP(_bt_end_vacuum_callback, PointerGetDatum(rel))
 */
BTCycleId
_bt_start_vacuum(Relation fc_rel)
{
	BTCycleId	fc_result;
	int			fc_i;
	BTOneVacInfo *fc_vac;

	LWLockAcquire(BtreeVacuumLock, LW_EXCLUSIVE);

	/*
	 * 分配下一个 cycle ID，注意避免零以及
	 * 保留的高值。
	 */
	fc_result = ++(btvacinfo->cycle_ctr);
	if (fc_result == 0 || fc_result > MAX_BT_CYCLE_ID)
		fc_result = btvacinfo->cycle_ctr = 1;

	/* 让我们确保这个索引没有已有条目 */
	for (fc_i = 0; fc_i < btvacinfo->num_vacuums; fc_i++)
	{
		fc_vac = &btvacinfo->vacuums[fc_i];
		if (fc_vac->relid.relId == fc_rel->rd_lockInfo.lockRelId.relId &&
			fc_vac->relid.dbId == fc_rel->rd_lockInfo.lockRelId.dbId)
		{
			/*
			 * 与后端大多数地方不同，我们必须显式
			 * 在抛出错误之前释放我们的 LWLock。
			 * 这是因为我们期望在事务
			 * 中止清理可以运行以释放 LWLocks 之前调用 _bt_end_vacuum()。
			 */
			LWLockRelease(BtreeVacuumLock);
			elog(ERROR, "multiple active vacuums for index \"%s\"",
				 RelationGetRelationName(fc_rel));
		}
	}

	/* 好的，添加一个条目 */
	if (btvacinfo->num_vacuums >= btvacinfo->max_vacuums)
	{
		LWLockRelease(BtreeVacuumLock);
		elog(ERROR, "out of btvacinfo slots");
	}
	fc_vac = &btvacinfo->vacuums[btvacinfo->num_vacuums];
	fc_vac->relid = fc_rel->rd_lockInfo.lockRelId;
	fc_vac->cycleid = fc_result;
	btvacinfo->num_vacuums++;

	LWLockRelease(BtreeVacuumLock);
	return fc_result;
}

/*
 * _bt_end_vacuum --- 将一个 btree VACUUM 操作标记为完成
 *
 * 注意：故意编写为如果未找到条目则不抱怨；
 * 这允许调用者在 start_vacuum 操作周围放置 PG_TRY。
 */
void _bt_end_vacuum(Relation fc_rel)
{
	int			fc_i;

	LWLockAcquire(BtreeVacuumLock, LW_EXCLUSIVE);

	/* 查找数组条目 */
	for (fc_i = 0; fc_i < btvacinfo->num_vacuums; fc_i++)
	{
		BTOneVacInfo *fc_vac = &btvacinfo->vacuums[fc_i];

		if (fc_vac->relid.relId == fc_rel->rd_lockInfo.lockRelId.relId &&
			fc_vac->relid.dbId == fc_rel->rd_lockInfo.lockRelId.dbId)
		{
			/* 通过向下移动最后一个条目来删除它 */
			*fc_vac = btvacinfo->vacuums[btvacinfo->num_vacuums - 1];
			btvacinfo->num_vacuums--;
			break;
		}
	}

	LWLockRelease(BtreeVacuumLock);
}

/*
 * 将 _bt_end_vacuum 包装为 on_shmem_exit 回调函数
 */
void _bt_end_vacuum_callback(int fc_code, Datum fc_arg)
{
	_bt_end_vacuum((Relation) DatumGetPointer(fc_arg));
}

/*
 * BTreeShmemSize --- 报告所需的共享内存空间量
 */
Size BTreeShmemSize(void)
{
	Size		fc_size;

	fc_size = offsetof(BTVacInfo, vacuums);
	fc_size = add_size(fc_size, mul_size(MaxBackends, sizeof(BTOneVacInfo)));
	return fc_size;
}

/*
 * BTreeShmemInit --- 初始化此模块的共享内存
 */
void BTreeShmemInit(void)
{
	bool		fc_found;

	btvacinfo = (BTVacInfo *) ShmemInitStruct("BTree Vacuum State",
											  BTreeShmemSize(),
											  &fc_found);

	if (!IsUnderPostmaster)
	{
		/* 初始化共享内存区域 */
		Assert(!fc_found);

		/*
		 * 循环计数器的起始值并不重要，但总是使用相同的起始值似乎不好。
		 * 应该使用 time() 的低序位位作为种子。
		 */
		btvacinfo->cycle_ctr = (BTCycleId) time(NULL);

		btvacinfo->num_vacuums = 0;
		btvacinfo->max_vacuums = MaxBackends;
	}
	else
		Assert(fc_found);
}

bytea * btoptions(Datum fc_reloptions, bool fc_validate)
{
	static const relopt_parse_elt fc_tab[] = {
		{"fillfactor", RELOPT_TYPE_INT, offsetof(BTOptions, fillfactor)},
		{"vacuum_cleanup_index_scale_factor", RELOPT_TYPE_REAL,
		offsetof(BTOptions, vacuum_cleanup_index_scale_factor)},
		{"deduplicate_items", RELOPT_TYPE_BOOL,
		offsetof(BTOptions, deduplicate_items)}
	};

	return (bytea *) build_reloptions(fc_reloptions, fc_validate,
									  RELOPT_KIND_BTREE,
									  sizeof(BTOptions),
									  fc_tab, lengthof(fc_tab));
}

/*
 *	btproperty() -- 检查索引的布尔属性。
 *
 * 这是可选的，但在这里处理 AMPROP_RETURNABLE 可以避免打开 rel 
 * 来调用 btcanreturn。
 */
bool btproperty(Oid fc_index_oid, int fc_attno,
		   IndexAMProperty fc_prop, const char *fc_propname,
		   bool *fc_res, bool *fc_isnull)
{
	switch (fc_prop)
	{
		case AMPROP_RETURNABLE:
			/* 仅针对列回答，而不是 AM 或整个索引 */
			if (fc_attno == 0)
				return false;
			/* 否则，btree 始终可以返回数据 */
			*fc_res = true;
			return true;

		default:
			return false;		/* 切换到通用代码 */
	}
}

/*
 *	btbuildphasename() -- 返回索引构建阶段的名称。
 */
char * btbuildphasename(int64 fc_phasenum)
{
	switch (fc_phasenum)
	{
		case PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE:
			return "initializing";
		case PROGRESS_BTREE_PHASE_INDEXBUILD_TABLESCAN:
			return "scanning table";
		case PROGRESS_BTREE_PHASE_PERFORMSORT_1:
			return "sorting live tuples";
		case PROGRESS_BTREE_PHASE_PERFORMSORT_2:
			return "sorting dead tuples";
		case PROGRESS_BTREE_PHASE_LEAF_LOAD:
			return "loading tuples in tree";
		default:
			return NULL;
	}
}

/*
 *	_bt_truncate() -- 创建没有不必要后缀属性的元组。
 *
 * 返回在调用者的内存上下文中分配的截断的枢轴索引元组，
 * 关键属性从调用者的 firstright 参数复制。如果 rel 是 
 * INCLUDE 索引，非关键属性一定会被截断，因为它们不是 
 * 关键空间的一部分。当显然返回的元组不需要一个或多个 
 * 后缀关键属性时，可以进行更激进的后缀截断。我们只需要 
 * 保留 firstright 属性直到并包括第一个非 lastleft-equal 
 * 属性。调用者的插入扫描键用于比较元组；此处不考虑 
 * 扫描键的参数值。
 *
 * 注意，返回的元组的 t_tid 偏移将包含存在的属性数量，
 * 因此原始项指针偏移未被表示。调用者应仅更改截断元组的 
 * 下行链接。还要注意，截断的关键属性在 _bt_compare() 
 * 中被视为包含 "负无穷" 值。
 *
 * 在最坏的情况下（当必须附加堆 TID 以区分 lastleft 和 
 * firstright 时），返回的元组的大小是 firstright 的大小 
 * 加上一个额外的 MAXALIGN() 的项指针的大小。这个保证
 * 是重要的，因为调用者需要保持在元组大小的 1/3 页面
 * 限制之下。如果此例程在属性/数据项内截断，它需要避免
 * 在截断 + TOAST 压缩导致最终数据项增大时返回增大的元组
 * 给调用者。
 */
IndexTuple _bt_truncate(Relation fc_rel, IndexTuple fc_lastleft, IndexTuple fc_firstright,
			 BTScanInsert fc_itup_key)
{
	TupleDesc	fc_itupdesc = RelationGetDescr(fc_rel);
	int16		fc_nkeyatts = IndexRelationGetNumberOfKeyAttributes(fc_rel);
	int			fc_keepnatts;
	IndexTuple	fc_pivot;
	IndexTuple	fc_tidpivot;
	ItemPointer fc_pivotheaptid;
	Size		fc_newsize;

	/*
	 * 我们应该只在叶页面上截断非枢轴元组。在拆分内部页面时
	 * 截断是绝对不可接受的。
	 */
	Assert(!BTreeTupleIsPivot(fc_lastleft) && !BTreeTupleIsPivot(fc_firstright));

	/* 确定在截断元组中必须保留多少属性 */
	fc_keepnatts = fc__bt_keep_natts(fc_rel, fc_lastleft, fc_firstright, fc_itup_key);

#ifdef DEBUG_NO_TRUNCATE
	/* 为了测试目的强制截断无效 */
	fc_keepnatts = fc_nkeyatts + 1;
#endif

	fc_pivot = index_truncate_tuple(fc_itupdesc, fc_firstright,
								 Min(fc_keepnatts, fc_nkeyatts));

	if (BTreeTupleIsPosting(fc_pivot))
	{
		/*
		 * index_truncate_tuple() 仅在没有属性需要截断时返回 
		 * firstright 的直接副本。当发生这种情况时，我们可能
		 * 需要在这里截断一个发布列表。
		 */
		Assert(fc_keepnatts == fc_nkeyatts || fc_keepnatts == fc_nkeyatts + 1);
		Assert(IndexRelationGetNumberOfAttributes(fc_rel) == fc_nkeyatts);
		fc_pivot->t_info &= ~INDEX_SIZE_MASK;
		fc_pivot->t_info |= MAXALIGN(BTreeTupleGetPostingOffset(fc_firstright));
	}

	/*
	 * 如果枢轴元组中有一个区分性的关键属性，我们就完成了
	 */
	if (fc_keepnatts <= fc_nkeyatts)
	{
		BTreeTupleSetNAtts(fc_pivot, fc_keepnatts, false);
		return fc_pivot;
	}

	/*
	 * 我们必须在新的枢轴元组中存储一个堆 TID，因为 firstright 中没有非 TID
	 * 键属性值可以区分分割的右侧和左侧。nbtree 将这种情况概念化为
	 * 无法截断任何键属性，因为堆 TID 被视为另一个键属性（尽管缺少 pg_attribute
	 * 条目）。
	 *
	 * 使用扩大空间来存放枢轴的副本。我们需要额外的空间
	 * 在最后存储一个堆 TID（使用特殊的枢轴元组
	 * 表示）。请注意，原始的枢轴在这一点上已经去除了 firstright 的
	 * 可能的发布列表/非键属性值。
	 */
	fc_newsize = MAXALIGN(IndexTupleSize(fc_pivot)) + MAXALIGN(sizeof(ItemPointerData));
	fc_tidpivot = palloc0(fc_newsize);
	memcpy(fc_tidpivot, fc_pivot, MAXALIGN(IndexTupleSize(fc_pivot)));
	/* 在这里不能泄漏内存 */
	pfree(fc_pivot);

	/*
	 * 在扩展的枢轴元组中存储 firstright 的所有键属性值加上 tiebreaker 堆
	 * TID 值
	 */
	fc_tidpivot->t_info &= ~INDEX_SIZE_MASK;
	fc_tidpivot->t_info |= fc_newsize;
	BTreeTupleSetNAtts(fc_tidpivot, fc_nkeyatts, true);
	fc_pivotheaptid = BTreeTupleGetHeapTID(fc_tidpivot);

	/*
	 * Lehman 和 Yao 在所有情况下都将 lastleft 用作叶子高键，但没有考虑后缀截断。
	 * 在没有截断的情况下遵循该示例似乎是个好主意——使用 lastleft 的堆
	 * TID。（这是唯一合法可用的最接近负无穷的值。）
	 */
	ItemPointerCopy(BTreeTupleGetMaxHeapTID(fc_lastleft), fc_pivotheaptid);

	/*
	 * 我们完成了。在返回之前，断言堆 TID 不变量保持有效。
	 *
	 * Lehman 和 Yao 要求向右页面的下行链接在页面分割的第二阶段
	 * 被插入到父页面中，并且是右页面上项目的严格下界，而左页面上项目
	 * 的非严格上界。断言堆 TID 遵循这些
	 * 不变量，因为显然需要堆 TID 值作为
	 * 决胜因素。
	 */
#ifndef DEBUG_NO_TRUNCATE
	Assert(ItemPointerCompare(BTreeTupleGetMaxHeapTID(fc_lastleft),
							  BTreeTupleGetHeapTID(fc_firstright)) < 0);
	Assert(ItemPointerCompare(fc_pivotheaptid,
							  BTreeTupleGetHeapTID(fc_lastleft)) >= 0);
	Assert(ItemPointerCompare(fc_pivotheaptid,
							  BTreeTupleGetHeapTID(fc_firstright)) < 0);
#else

	/*
	 * 这些不变量在考虑 lastleft + firstright
	 * 堆 TID 属性值时不保证有效，仅仅因为定义了 DEBUG_NO_TRUNCATE
	 * （堆 TID 可能实际并不需要作为决胜因素）。因此，DEBUG_NO_TRUNCATE
	 * 必须使用一个能够始终作为右侧项目的严格下界的堆 TID 值。
	 * 特别是，当 lastleft 的 TID 恰好大于 firstright 的 TID 时，它必须避免
	 * 使用 firstright 的前导键
	 * 属性值以及 lastleft 的堆 TID 值。
	 */
	ItemPointerCopy(BTreeTupleGetHeapTID(fc_firstright), fc_pivotheaptid);

	/*
	 * 枢轴堆 TID 不应该完全等于 firstright。请注意，
	 * 当这是唯一可用值时，枢轴堆 TID 最终仍将等于 lastleft 的堆 TID。
	 */
	ItemPointerSetOffsetNumber(fc_pivotheaptid,
							   OffsetNumberPrev(ItemPointerGetOffsetNumber(fc_pivotheaptid)));
	Assert(ItemPointerCompare(fc_pivotheaptid,
							  BTreeTupleGetHeapTID(fc_firstright)) < 0);
#endif

	return fc_tidpivot;
}

/*
 * _bt_keep_natts - 在截断时要保留多少个键属性。
 *
 * 调用者提供两个元组来封闭一个分割点。调用者的插入
 * scankey 用于比较元组；scankey 的参数值在这里不考虑。
 *
 * 这可以返回一个比索引关系中键属性数量多一的属性数量。
 * 这表明调用者必须在新的枢轴元组中使用堆 TID 作为唯一标识符。
 */
static int fc__bt_keep_natts(Relation fc_rel, IndexTuple fc_lastleft, IndexTuple fc_firstright,
			   BTScanInsert fc_itup_key)
{
	int			fc_nkeyatts = IndexRelationGetNumberOfKeyAttributes(fc_rel);
	TupleDesc	fc_itupdesc = RelationGetDescr(fc_rel);
	int			fc_keepnatts;
	ScanKey		fc_scankey;

	/*
	 * _bt_compare() 将截断的键属性视为负无穷值，这会破坏在 !heapkeyspace 索引中的搜索。然而，我们仍然必须截断非键属性值。
	 */
	if (!fc_itup_key->heapkeyspace)
		return fc_nkeyatts;

	fc_scankey = fc_itup_key->scankeys;
	fc_keepnatts = 1;
	for (int fc_attnum = 1; fc_attnum <= fc_nkeyatts; fc_attnum++, fc_scankey++)
	{
		Datum		fc_datum1,
					fc_datum2;
		bool		fc_isNull1,
					fc_isNull2;

		fc_datum1 = index_getattr(fc_lastleft, fc_attnum, fc_itupdesc, &fc_isNull1);
		fc_datum2 = index_getattr(fc_firstright, fc_attnum, fc_itupdesc, &fc_isNull2);

		if (fc_isNull1 != fc_isNull2)
			break;

		if (!fc_isNull1 &&
			DatumGetInt32(FunctionCall2Coll(&fc_scankey->sk_func,
											fc_scankey->sk_collation,
											fc_datum1,
											fc_datum2)) != 0)
			break;

		fc_keepnatts++;
	}

	/*
	 * 确认 _bt_keep_natts_fast() 在通过时与我们一致。这在 allequalimage 索引中是预期的。
	 */
	Assert(!fc_itup_key->allequalimage ||
		   fc_keepnatts == _bt_keep_natts_fast(fc_rel, fc_lastleft, fc_firstright));

	return fc_keepnatts;
}

/*
 * _bt_keep_natts_fast - _bt_keep_natts 的快速按位变体。
 *
 * 这被导出以便在找到拆分位置时，可以廉价地评估候选拆分点对后缀截断的影响。使用简单的按位方法进行数据比较以节省周期。
 *
 * 此处采用的方法通常会提供与 _bt_keep_natts 相同的答案（对于来自 heapkeyspace 索引的同一对元组），因为大多数 btree opclasses 只能在解冻后表明两个数据项相等的情况。在索引仅具有“相等图像”列时，此例程保证与 _bt_keep_natts 的结果相同。
 *
 * 调用者可以依赖于这里认为相等的属性在 _bt_keep_natts 中也是绝对相等的，即使索引使用的 opclass 或排序规则不是“allequalimage”/去重安全的。这种较弱的保证对于 nbtsplitloc.c 调用者来说足够好，因为假阴性通常只会使叶页面拆分使用更平衡的拆分点。
 */
int _bt_keep_natts_fast(Relation fc_rel, IndexTuple fc_lastleft, IndexTuple fc_firstright)
{
	TupleDesc	fc_itupdesc = RelationGetDescr(fc_rel);
	int			fc_keysz = IndexRelationGetNumberOfKeyAttributes(fc_rel);
	int			fc_keepnatts;

	fc_keepnatts = 1;
	for (int fc_attnum = 1; fc_attnum <= fc_keysz; fc_attnum++)
	{
		Datum		fc_datum1,
					fc_datum2;
		bool		fc_isNull1,
					fc_isNull2;
		Form_pg_attribute fc_att;

		fc_datum1 = index_getattr(fc_lastleft, fc_attnum, fc_itupdesc, &fc_isNull1);
		fc_datum2 = index_getattr(fc_firstright, fc_attnum, fc_itupdesc, &fc_isNull2);
		fc_att = TupleDescAttr(fc_itupdesc, fc_attnum - 1);

		if (fc_isNull1 != fc_isNull2)
			break;

		if (!fc_isNull1 &&
			!datum_image_eq(fc_datum1, fc_datum2, fc_att->attbyval, fc_att->attlen))
			break;

		fc_keepnatts++;
	}

	return fc_keepnatts;
}

/*
 *  _bt_check_natts() -- 验证元组具有预期数量的属性。
 *
 * 返回值指示在页面的特定偏移量上是否找到了预期数量的属性。这可以用作通用的合理性检查。
 *
 * 直接使用 BTreeTupleGetNAtts() 测试元组通常应该优于这里的调用。这通常更方便，并且总是更明确。当 offnum 的元组可能是一个使用 pre-v11 磁盘表示的负无穷元组，或者当低上下文检查合适时，请在此调用。此例程尽可能严格地定义每个版本的 btree 中预期的内容。
 */
bool _bt_check_natts(Relation fc_rel, bool fc_heapkeyspace, Page fc_page, OffsetNumber fc_offnum)
{
	int16		fc_natts = IndexRelationGetNumberOfAttributes(fc_rel);
	int16		fc_nkeyatts = IndexRelationGetNumberOfKeyAttributes(fc_rel);
	BTPageOpaque fc_opaque = BTPageGetOpaque(fc_page);
	IndexTuple	fc_itup;
	int			fc_tupnatts;

	/*
	 * 我们无法可靠地测试已删除或半死的页面，因为它们具有虚拟的高键
	 */
	if (P_IGNORE(fc_opaque))
		return true;

	Assert(fc_offnum >= FirstOffsetNumber &&
		   fc_offnum <= PageGetMaxOffsetNumber(fc_page));

	/*
	 * 为索引元组中的键数分配的掩码必须能够容纳最大可能数量的索引属性
	 */
	StaticAssertStmt(BT_OFFSET_MASK >= INDEX_MAX_KEYS,
					 "BT_OFFSET_MASK can't fit INDEX_MAX_KEYS");

	fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_offnum));
	fc_tupnatts = BTreeTupleGetNAtts(fc_itup, fc_rel);

	/* !heapkeyspace 索引不支持去重 */
	if (!fc_heapkeyspace && BTreeTupleIsPosting(fc_itup))
		return false;

	/* 发布列表元组的 "pivot heap TID" 位永远不应被设置 */
	if (BTreeTupleIsPosting(fc_itup) &&
		(ItemPointerGetOffsetNumberNoCheck(&fc_itup->t_tid) &
		 BT_PIVOT_HEAP_TID_ATTR) != 0)
		return false;

	/* INCLUDE 索引不支持去重 */
	if (fc_natts != fc_nkeyatts && BTreeTupleIsPosting(fc_itup))
		return false;

	if (P_ISLEAF(fc_opaque))
	{
		if (fc_offnum >= P_FIRSTDATAKEY(fc_opaque))
		{
			/*
			 * 非枢轴元组不应被显式标记为枢轴元组
			 */
			if (BTreeTupleIsPivot(fc_itup))
				return false;

			/*
			 * 不是页面高键的叶元组（非枢轴元组）永远不应被截断。（请注意，即使是发布列表元组，tupnatts 也必须已经被推断，因为只有枢轴元组直接存储 tupnatts。）
			 */
			return fc_tupnatts == fc_natts;
		}
		else
		{
			/*
			 * 右侧页面不包含页面高键，因此元组在上面被作为普通叶元组检查
			 */
			Assert(!P_RIGHTMOST(fc_opaque));

			/*
			 * !heapkeyspace 高键元组仅包含键属性。注意，tupnatts 仅会在某些恰好具有非键属性的 !heapkeyspace 索引中明确表示。
			 */
			if (!fc_heapkeyspace)
				return fc_tupnatts == fc_nkeyatts;

			/* 使用通用的 heapkeyspace 轴心元组处理 */
		}
	}
	else						/* !P_ISLEAF(opaque) */
	{
		if (fc_offnum == P_FIRSTDATAKEY(fc_opaque))
		{
			/*
			 * 任何内部页面上的第一个元组（可能是其高键之后的第一个）是其负无穷元组。负无穷元组始终截断为零属性。它们是一种特定类型的轴心元组。
			 */
			if (fc_heapkeyspace)
				return fc_tupnatts == 0;

			/*
			 * 如果负无穷元组是在 Postgres v11 之前的页面拆分过程中生成的，则属性数量将不会被明确表示。当存在一个非零的显式表示时，或者当没有显式表示且元组显然不是 pre-pg_upgrade 元组时，必须存在问题。
			 *
			 * 在 v11 之前，下链接始终具有 P_HIKEY 作为其偏移。接受此作为有效 !heapkeyspace 负无穷元组的替代指示。
			 */
			return fc_tupnatts == 0 ||
				ItemPointerGetOffsetNumber(&(fc_itup->t_tid)) == P_HIKEY;
		}
		else
		{
			/*
			 * 带有分隔符键的 !heapkeyspace 下链接元组仅包含键属性。注意，tupnatts 仅会在某些恰好具有非键属性的 !heapkeyspace 索引中明确表示。
			 */
			if (!fc_heapkeyspace)
				return fc_tupnatts == fc_nkeyatts;

			/* 使用通用的 heapkeyspace 轴心元组处理 */
		}
	}

	/* 处理 heapkeyspace 轴心元组（不包括负无穷项） */
	Assert(fc_heapkeyspace);

	/*
	 * heapkeyspace 索引轴心元组必须明确表示属性数量，无论是否存在非键属性。
	 */
	if (!BTreeTupleIsPivot(fc_itup))
		return false;

	/* 轴心元组不应使用发布列表表示（冗余） */
	if (BTreeTupleIsPosting(fc_itup))
		return false;

	/*
	 * Heap TID 是一个打破平局的键属性，因此在任何其他键属性被截断时，它不能被截断
	 */
	if (BTreeTupleGetHeapTID(fc_itup) != NULL && fc_tupnatts != fc_nkeyatts)
		return false;

	/*
	 * 轴心元组必须至少有一个未截断的键属性（负无穷轴心元组是唯一的例外）。轴心元组不能表示键属性的值存在超出 pg_index.indnkeyatts 的情况。
	 */
	return fc_tupnatts > 0 && fc_tupnatts <= fc_nkeyatts;
}

/*
 *
 *  _bt_check_third_page() -- 检查元组是否可以在 btree 页面上适合。
 *
 * 我们实际上需要能够在每个页面上放置三个项，因此将任何一项限制为每页可用空间的 1/3。注意，itemsz 不应包括 ItemId 的开销。
 *
 * 在这里应用 TOAST 方法可能是有用的，而不是抛出错误。不过，使用离线存储会破坏后缀截断和 contrib/amcheck 的假设。
 */
void _bt_check_third_page(Relation fc_rel, Relation fc_heap, bool fc_needheaptidspace,
					 Page fc_page, IndexTuple fc_newtup)
{
	Size		fc_itemsz;
	BTPageOpaque fc_opaque;

	fc_itemsz = MAXALIGN(IndexTupleSize(fc_newtup));

	/* 再次检查项大小是否超出限制 */
	if (fc_itemsz <= BTMaxItemSize(fc_page))
		return;

	/*
	 * 元组可能太大而无法放在页面上，但索引可能使用版本 2 或版本 3，或者页面是内部页面，在这种情况下适用稍高的限制。
	 */
	if (!fc_needheaptidspace && fc_itemsz <= BTMaxItemSizeNoHeapTid(fc_page))
		return;

	/*
	 * 内部页面插入在这里不能失败，因为那将意味着一次早期的叶子级别插入应该失败但没有失败。
	 */
	fc_opaque = BTPageGetOpaque(fc_page);
	if (!P_ISLEAF(fc_opaque))
		elog(ERROR, "cannot insert oversized tuple of size %zu on internal page of index \"%s\"",
			 fc_itemsz, RelationGetRelationName(fc_rel));

	ereport(ERROR,
			(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
			 errmsg("index row size %zu exceeds btree version %u maximum %zu for index \"%s\"",
					fc_itemsz,
					fc_needheaptidspace ? BTREE_VERSION : BTREE_NOVAC_VERSION,
					fc_needheaptidspace ? BTMaxItemSize(fc_page) :
					BTMaxItemSizeNoHeapTid(fc_page),
					RelationGetRelationName(fc_rel)),
			 errdetail("Index row references tuple (%u,%u) in relation \"%s\".",
					   ItemPointerGetBlockNumber(BTreeTupleGetHeapTID(fc_newtup)),
					   ItemPointerGetOffsetNumber(BTreeTupleGetHeapTID(fc_newtup)),
					   RelationGetRelationName(fc_heap)),
			 errhint("Values larger than 1/3 of a buffer page cannot be indexed.\n"
					 "Consider a function index of an MD5 hash of the value, "
					 "or use full text indexing."),
			 errtableconstraint(fc_heap, RelationGetRelationName(fc_rel))));
}

/*
 * 关系中的所有属性是否都是“相等是图像相等”属性？
 *
 * 我们使用每个属性的 BTEQUALIMAGE_PROC 操作类函数。如果任何操作类缺少 BTEQUALIMAGE_PROC 函数或返回 false，我们就返回 false；否则返回 true。
 *
 * 返回的布尔值在索引构建过程中存储在索引元页面中。
 * 只有当我们返回 true 时，去重才能使用。
 */
bool _bt_allequalimage(Relation fc_rel, bool fc_debugmessage)
{
	bool		fc_allequalimage = true;

	/* INCLUDE 索引永远不支持去重 */
	if (IndexRelationGetNumberOfAttributes(fc_rel) !=
		IndexRelationGetNumberOfKeyAttributes(fc_rel))
		return false;

	for (int fc_i = 0; fc_i < IndexRelationGetNumberOfKeyAttributes(fc_rel); fc_i++)
	{
		Oid			fc_opfamily = fc_rel->rd_opfamily[fc_i];
		Oid			fc_opcintype = fc_rel->rd_opcintype[fc_i];
		Oid			fc_collation = fc_rel->rd_indcollation[fc_i];
		Oid			fc_equalimageproc;

		fc_equalimageproc = get_opfamily_proc(fc_opfamily, fc_opcintype, fc_opcintype,
										   BTEQUALIMAGE_PROC);

		/*
		 * 如果没有 BTEQUALIMAGE_PROC，则假设去重是不安全的。否则，实际调用 proc 并查看它的返回值。
		 */
		if (!OidIsValid(fc_equalimageproc) ||
			!DatumGetBool(OidFunctionCall1Coll(fc_equalimageproc, fc_collation,
											   ObjectIdGetDatum(fc_opcintype))))
		{
			fc_allequalimage = false;
			break;
		}
	}

	if (fc_debugmessage)
	{
		if (fc_allequalimage)
			elog(DEBUG1, "index \"%s\" can safely use deduplication",
				 RelationGetRelationName(fc_rel));
		else
			elog(DEBUG1, "index \"%s\" cannot use deduplication",
				 RelationGetRelationName(fc_rel));
	}

	return fc_allequalimage;
}
